deadbyte-mcp 0.15.44 → 0.15.46

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.
Files changed (174) hide show
  1. package/MANIFEST.SHA256 +86 -138
  2. package/PROVENANCE.json +35 -18
  3. package/README.txt +22 -21
  4. package/RELEASE.json +369 -334
  5. package/SBOM.cdx.json +2 -2
  6. package/TEST-MATRIX.json +27 -17
  7. package/bridge/package.json +1 -1
  8. package/controller/deadbyte-controller.ps1 +1871 -1769
  9. package/controller/deadbyte-desktop-policy.json +6 -6
  10. package/controller/deadbyte-process-policy.json +4 -4
  11. package/docs/R68-COMPAT-HOST-EXEC-CANDIDATE.md +63 -0
  12. package/docs/R69-LIVE-OUTCOME-R70-POLICY-TRANSFER.md +136 -0
  13. package/docs/R70-CODING-DIGEST-SUPERVISOR-CHECKPOINT-20261013.md +42 -0
  14. package/docs/R70-FINITE-GRANT-OWNER-BOUNDARY-20261013.md +80 -0
  15. package/docs/R70-OWNER-BOOTSTRAP-CHECKPOINT-20261013.md +60 -0
  16. package/docs/R70-REFERENCE-MECHANISMS.json +95 -0
  17. package/docs/R70-TRUST-GUARD-EXPIRY-CHECKPOINT-20261013.md +47 -0
  18. package/package.json +2 -2
  19. package/scripts/benchmark-l0-r71.mjs +76 -0
  20. package/scripts/gate-windows-r71.ps1 +64 -0
  21. package/scripts/gate-windows-r72.ps1 +76 -0
  22. package/scripts/npm-release-r48.mjs +4 -0
  23. package/scripts/npm-release-readiness-r71.mjs +21 -0
  24. package/scripts/npm-release-readiness-r72.mjs +76 -0
  25. package/scripts/verify-r70-full-regression.ps1 +72 -0
  26. package/src/authority-continuity-r65.mjs +28 -8
  27. package/src/autonomous-mcp-tools.mjs +1 -1
  28. package/src/autonomous-output-contracts.mjs +4 -1
  29. package/src/autonomous-policy.mjs +54 -7
  30. package/src/capability-ledger.mjs +1 -1
  31. package/src/capability-resolver.mjs +2 -1
  32. package/src/commander-compat-runtime.mjs +30 -2
  33. package/src/deadbyte-cli.mjs +722 -719
  34. package/src/launcher-isolation.mjs +58 -0
  35. package/src/native-diff-v2.mjs +18 -5
  36. package/src/native-diff.mjs +21 -5
  37. package/src/r70-owner-approval.mjs +78 -0
  38. package/src/remote-authority.mjs +75 -75
  39. package/src/remote-live-log.mjs +48 -14
  40. package/src/remote-mcp-machine-smoke-client.mjs +84 -84
  41. package/src/remote-result-journal.mjs +1 -1
  42. package/src/runtime-update.mjs +232 -24
  43. package/src/staging-input-isolation.mjs +79 -0
  44. package/src/tool-audit.mjs +2 -0
  45. package/src/version.mjs +1 -1
  46. package/test/autonomous-local-supervisor.test.mjs +2 -1
  47. package/test/autonomous-output-contract.test.mjs +17 -1
  48. package/test/autonomous-supervisor-integration.test.mjs +2 -1
  49. package/test/capability-resolver.test.mjs +17 -0
  50. package/test/codex-autonomous-r44.test.mjs +1 -1
  51. package/test/commander-compat-runtime.test.mjs +42 -0
  52. package/test/controller-policy-preflight-r59.test.mjs +1 -0
  53. package/test/deferred-slot-operation-behavior.test.mjs +2 -2
  54. package/test/native-diff-topology-r36.test.mjs +35 -2
  55. package/test/native-diff.test.mjs +83 -3
  56. package/test/r58-cleanup-retention.test.mjs +3 -3
  57. package/test/r58-concurrent-recovery.test.mjs +3 -3
  58. package/test/r58-stale-owner.test.mjs +1 -1
  59. package/test/r59-tool-surface-compat.test.mjs +9 -5
  60. package/test/r60-authority-coherence.test.mjs +10 -2
  61. package/test/r65-authority-runtime-epoch.test.mjs +9 -1
  62. package/test/r67-isolated-slot-rollback-restore.test.mjs +2 -1
  63. package/test/r67-npm-publish-gate.test.mjs +4 -2
  64. package/test/r67-release-contract.test.mjs +8 -7
  65. package/test/r69-live-outcome.test.mjs +137 -0
  66. package/test/r70-autonomous-grant-expiry.test.mjs +89 -0
  67. package/test/r70-autonomous-policy-bootstrap.test.mjs +166 -0
  68. package/test/r70-controller-autonomous-root.test.mjs +104 -0
  69. package/test/r70-controller-grant-subset.test.mjs +28 -0
  70. package/test/r70-controller-owner-issue-boundary.test.mjs +80 -0
  71. package/test/r70-controller-policy-snapshot.test.mjs +66 -0
  72. package/test/r70-owner-approval.test.mjs +67 -0
  73. package/test/r71-full-verifier-artifact.test.mjs +28 -0
  74. package/test/r71-isolated-slot-rollback-restore.test.mjs +62 -0
  75. package/test/r71-native-diff-mcp-wire.test.mjs +75 -0
  76. package/test/r71-publish-fence.test.mjs +38 -0
  77. package/test/r71-release-identity.test.mjs +33 -0
  78. package/test/r72-launcher-isolation.test.mjs +99 -0
  79. package/test/r72-publication-fence.test.mjs +56 -0
  80. package/test/r72-publication-output-integrity.test.mjs +110 -0
  81. package/test/r72-remote-safe-contract.test.mjs +101 -0
  82. package/test/r72-staging-input-isolation.test.mjs +87 -0
  83. package/test/r72-staging-workspace-isolation.test.mjs +114 -0
  84. package/test/remote-result-journal.test.mjs +12 -0
  85. package/test/remote-session-cli.test.mjs +112 -65
  86. package/test/standing-grants.test.mjs +2 -2
  87. package/test/tool-audit.test.mjs +14 -0
  88. package/bin/appcontainer-stage.obj +0 -0
  89. package/bin/bootstrap-advapi32.obj +0 -0
  90. package/bin/bootstrap-exitcode.obj +0 -0
  91. package/bin/bootstrap-kernel32.obj +0 -0
  92. package/bin/child-control-probe.obj +0 -0
  93. package/bin/child-control-stage.obj +0 -0
  94. package/bin/contained-reverse-worker.obj +0 -0
  95. package/bin/contained-transform-worker.obj +0 -0
  96. package/bin/containment-probe.obj +0 -0
  97. package/bin/deadbyte-contain.obj +0 -0
  98. package/bin/deadbyte-desktop-input-r40.obj +0 -0
  99. package/bin/deadbyte-desktop-observer-r40.obj +0 -0
  100. package/bin/deadbyte-desktop-uia-r40.obj +0 -0
  101. package/bin/deadbyte-exec.obj +0 -0
  102. package/bin/deadbyte-hardware-observer-r45.obj +0 -0
  103. package/bin/deadbyte-native-inspect-r67.obj +0 -0
  104. package/bin/deadbyte-process-host.obj +0 -0
  105. package/bin/deadbyte-tunnel-host.obj +0 -0
  106. package/conpty-input-observer.obj +0 -0
  107. package/miniconpty.obj +0 -0
  108. package/proof/CONTAINMENT-BUILD.txt +0 -163
  109. package/proof/MCP-SMOKE-STATUS.txt +0 -9
  110. package/proof/NODE-SYNTAX.txt +0 -4
  111. package/proof/PROOF.txt +0 -6
  112. package/proof/R34-TASK8-SUPERVISOR-CHECKPOINT.json +0 -29
  113. package/proof/R34-TASK9-MCP-SURFACE-CHECKPOINT.json +0 -28
  114. package/proof/SECRET-SCAN.txt +0 -4
  115. package/proof/SOURCE-GATES.txt +0 -16
  116. package/proof/STATUS.txt +0 -11
  117. package/proof/TARGET-V0313-CONTAINMENT-PASS.sha256 +0 -1
  118. package/proof/TARGET-V0313-CONTAINMENT-PASS.txt +0 -215
  119. package/proof/TARGET-V040-CONTAINED-IPC-FAIL.sha256 +0 -1
  120. package/proof/TARGET-V040-CONTAINED-IPC-FAIL.txt +0 -299
  121. package/proof/TARGET-V041-CONTAINED-CREATEPROCESS-203.sha256 +0 -1
  122. package/proof/TARGET-V041-CONTAINED-CREATEPROCESS-203.txt +0 -311
  123. package/proof/TARGET-V042-CONTAINED-CREATEPROCESS-203.sha256 +0 -1
  124. package/proof/TARGET-V042-CONTAINED-CREATEPROCESS-203.txt +0 -313
  125. package/proof/TARGET-V043-CONTAINED-CREATEPROCESS-203.sha256 +0 -1
  126. package/proof/TARGET-V043-CONTAINED-CREATEPROCESS-203.txt +0 -308
  127. package/proof/TARGET-V044-CONTAINED-PASS.sha256 +0 -1
  128. package/proof/TARGET-V044-CONTAINED-PASS.txt +0 -432
  129. package/proof/TARGET-V045-DUAL-OPERATION-PASS.sha256 +0 -1
  130. package/proof/TARGET-V045-DUAL-OPERATION-PASS.txt +0 -481
  131. package/proof/TARGET-V046-FIXED-EXECUTABLE-REGISTRY-PASS.sha256 +0 -1
  132. package/proof/TARGET-V046-FIXED-EXECUTABLE-REGISTRY-PASS.txt +0 -610
  133. package/proof/TARGET-V047-RUNTIME-MANIFEST-PASS.sha256 +0 -1
  134. package/proof/TARGET-V047-RUNTIME-MANIFEST-PASS.txt +0 -642
  135. package/proof/TARGET-V050-BATCH-GATE-PASS.sha256 +0 -1
  136. package/proof/TARGET-V050-BATCH-GATE-PASS.txt +0 -713
  137. package/proof/TESTS.txt +0 -686
  138. package/proof/TRUST-EPHEMERAL.txt +0 -4
  139. package/proof/V03-FREEZE.txt +0 -27
  140. package/proof/V041-IPC-HOTFIX.txt +0 -16
  141. package/proof/V042-ENV-HOTFIX.txt +0 -15
  142. package/proof/V043-DRIVE-CWD-HOTFIX.txt +0 -14
  143. package/proof/V044-LOCALAPPDATA-HOTFIX.txt +0 -18
  144. package/proof/V045-OPERATION-REGISTRY.txt +0 -7
  145. package/proof/V046-WORKER-REGISTRY.txt +0 -12
  146. package/proof/V047-WORKER-MANIFEST.txt +0 -18
  147. package/proof/V050-BATCH-TRUST-CLOSURE.txt +0 -21
  148. package/proof/V060-ACTIVE-NETWORK-AUTHORITY.txt +0 -29
  149. package/proof/V070-NODE-SYNTAX.txt +0 -36
  150. package/proof/V070-NODE-TESTS.txt +0 -752
  151. package/proof/V070-PACK-DRY-RUN.txt +0 -126
  152. package/proof/V070-SOURCE-GATE.txt +0 -30
  153. package/proof/V070-STATIC-SCAN.txt +0 -4
  154. package/proof/V070-WINDOWS-GATE-PENDING.txt +0 -25
  155. package/proof/V071-NODE-SYNTAX.txt +0 -1
  156. package/proof/V071-NODE-TESTS.txt +0 -796
  157. package/proof/V071-PACK-DRY-RUN.json +0 -656
  158. package/proof/V071-SOURCE-GATE.txt +0 -16
  159. package/proof/V071-WINDOWS-GATE-PENDING.txt +0 -24
  160. package/proof/V073-NODE-SYNTAX.txt +0 -4
  161. package/proof/V073-SOURCE-GATE.txt +0 -21
  162. package/proof/V073-STATIC-SCAN.txt +0 -10
  163. package/proof/V073-WINDOWS-GATE-PENDING.txt +0 -5
  164. package/proof/VERIFY.txt +0 -7
  165. package/proof/WINDOWS-CONTAINED-STATUS.txt +0 -5
  166. package/proof/WINDOWS-CONTAINMENT-STATUS.txt +0 -3
  167. package/proof/commander-closure-20260927/CONTINUATION.json +0 -43
  168. package/proof/commander-closure-20260927/RESULT.json +0 -30
  169. package/proof/commander-closure-20260927/SEARCH-CONTINUATION.json +0 -30
  170. package/proof/commander-closure-20260927/XLS-REFERENCE-PROBE.json +0 -13
  171. package/proof/commander-closure-20260928/DIFFERENTIAL-CHECKPOINT.json +0 -122
  172. package/proof/r59-full-regression-current.json +0 -13
  173. package/proof/r59-full-regression-current.stderr.log +0 -0
  174. package/proof/r59-full-regression-current.stdout.log +0 -1268
@@ -1,1771 +1,1873 @@
1
- #requires -Version 5.1
2
- [CmdletBinding()]
3
- param(
4
- [Parameter(Position=0)]
5
- [ValidateSet('menu','start','resume','park','stop','restart','status','url','test','fulltest','toggle','clean','machinetest','armprocess','disarmprocess','processstatus','armhost','disarmhost','hoststatus','hosttest','armcoding','disarmcoding','codingstatus','codingtest','armautonomous','disarmautonomous','autonomousstatus','autonomoustest','supervisorstart','supervisorstop','supervisorstatus','supervisorrecover','armdesktop','disarmdesktop','desktopstatus')]
6
- [string]$Command = 'menu',
7
- [switch]$Json
8
- )
9
-
10
- Set-StrictMode -Version 3.0
11
- $ErrorActionPreference = 'Stop'
12
-
13
- $ControllerSchema = 'deadbyte.controller.v1'
14
- $Root = Split-Path -Parent $MyInvocation.MyCommand.Path
15
- $RuntimeDir = if ($env:DEADBYTE_CONTROLLER_RUNTIME_ROOT) { [System.IO.Path]::GetFullPath([string]$env:DEADBYTE_CONTROLLER_RUNTIME_ROOT) } else { Join-Path $Root 'runtime' }
16
- $LogDir = Join-Path $RuntimeDir 'logs'
17
- $ConfigPath = if ($env:DEADBYTE_CONTROLLER_CONFIG) { $env:DEADBYTE_CONTROLLER_CONFIG } else { Join-Path $Root 'deadbyte-controller.config.psd1' }
18
-
19
- function Fail([string]$Message) { throw "DEADBYTE controller: $Message" }
20
- if ($Json -and $Command -ne 'status') { Fail 'JSON observation is available only for status' }
21
-
22
- function Get-ControllerMutationMutexName {
23
- $normalized = ([System.IO.Path]::GetFullPath($RuntimeDir)).TrimEnd('\\').ToLowerInvariant()
24
- $sha = [System.Security.Cryptography.SHA256]::Create()
25
- try {
26
- $bytes = [System.Text.Encoding]::UTF8.GetBytes($normalized)
27
- $digest = $sha.ComputeHash($bytes)
28
- } finally {
29
- $sha.Dispose()
30
- }
31
- $hex = ([System.BitConverter]::ToString($digest)).Replace('-', '').ToLowerInvariant().Substring(0, 24)
32
- return "Local\DEADBYTE-MCP-Controller-$hex"
33
- }
34
-
35
- function Invoke-WithControllerMutationLock([string]$Name, [scriptblock]$Body) {
36
- $mutexName = Get-ControllerMutationMutexName
37
- $mutex = New-Object System.Threading.Mutex($false, $mutexName)
38
- $held = $false
39
- try {
40
- try {
41
- $held = $mutex.WaitOne([TimeSpan]::FromSeconds(180))
42
- } catch [System.Threading.AbandonedMutexException] {
43
- $held = $true
44
- }
45
- if (-not $held) { Fail "controller mutation lock timeout for '$Name'" }
46
- & $Body
47
- } finally {
48
- if ($held) {
49
- try { $mutex.ReleaseMutex() } catch {}
50
- }
51
- $mutex.Dispose()
52
- }
53
- }
54
-
55
- function Ensure-Directory([string]$Path) { if (-not (Test-Path -LiteralPath $Path -PathType Container)) { New-Item -ItemType Directory -Path $Path -Force | Out-Null } }
56
- function Ledger-Path([string]$Name) { Join-Path $RuntimeDir ("{0}.instance.json" -f $Name.ToLowerInvariant()) }
57
- function Log-Path([string]$Name, [string]$Kind) { Join-Path $LogDir ("{0}.{1}.log" -f $Name.ToLowerInvariant(), $Kind) }
58
- function Controller-StartFailurePath { Join-Path $RuntimeDir 'controller-start-failure.json' }
59
- function Read-LogTailBounded([string]$Path, [int]$MaxChars = 8192) {
60
- if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return '' }
61
- try {
62
- $text = (@(Get-Content -LiteralPath $Path -Encoding UTF8 -Tail 80 -ErrorAction Stop) -join [Environment]::NewLine)
63
- if ($text.Length -gt $MaxChars) { return $text.Substring($text.Length - $MaxChars) }
64
- return $text
65
- } catch { return '' }
66
- }
67
- function Write-ControllerStartFailure([string]$Name, [System.Exception]$Exception) {
68
- Ensure-Directory $RuntimeDir; Ensure-Directory $LogDir
69
- $body = [ordered]@{
70
- schema = 'deadbyte.controller-start-failure.v1'
71
- command = $Name
72
- failed_at_utc = [DateTime]::UtcNow.ToString('o')
73
- message = [string]$Exception.Message
74
- bridge_stdout_tail = Read-LogTailBounded (Log-Path 'Bridge' 'stdout')
75
- bridge_stderr_tail = Read-LogTailBounded (Log-Path 'Bridge' 'stderr')
76
- cloud_stdout_tail = Read-LogTailBounded (Log-Path 'Cloud' 'stdout')
77
- cloud_stderr_tail = Read-LogTailBounded (Log-Path 'Cloud' 'stderr')
78
- }
79
- $path = Controller-StartFailurePath
80
- $tmp = $path + '.tmp'
81
- $json = $body | ConvertTo-Json -Depth 4
82
- [System.IO.File]::WriteAllText($tmp, $json, (New-Object System.Text.UTF8Encoding($false)))
83
- Move-Item -LiteralPath $tmp -Destination $path -Force
84
- }
85
-
86
-
87
- function Read-TextFileSafe([string]$Path) {
88
- if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return '' }
89
- $text = Get-Content -LiteralPath $Path -Raw -Encoding UTF8
90
- if ($null -eq $text) { return '' }
91
- return [string]$text
92
- }
93
-
94
- function Get-Config {
95
- if (-not (Test-Path -LiteralPath $ConfigPath -PathType Leaf)) {
96
- Fail "missing trusted config '$ConfigPath'; copy deadbyte-controller.config.psd1.example and set exact long-lived executable paths"
97
- }
98
- $cfg = Import-PowerShellDataFile -LiteralPath $ConfigPath
99
- if ($cfg.Schema -ne $ControllerSchema) { Fail 'controller config schema mismatch' }
100
- $coreRoot = if ($cfg.ContainsKey('CoreRoot') -and -not [string]::IsNullOrWhiteSpace([string]$cfg.CoreRoot)) { [string]$cfg.CoreRoot } else { Split-Path -Parent $Root }
101
- $coreRoot = [System.IO.Path]::GetFullPath($coreRoot)
102
- if (-not (Test-Path -LiteralPath (Join-Path $coreRoot 'package.json') -PathType Leaf) -or -not (Test-Path -LiteralPath (Join-Path $coreRoot 'src\mcp-server.mjs') -PathType Leaf)) {
103
- Fail "CoreRoot is not a DEADBYTE MCP package: $coreRoot"
104
- }
105
- $cfg['CoreRoot'] = $coreRoot
106
- $workspaceRoot = if ($cfg.ContainsKey('WorkspaceRoot') -and -not [string]::IsNullOrWhiteSpace([string]$cfg.WorkspaceRoot)) { [string]$cfg.WorkspaceRoot } else { Join-Path $coreRoot 'workspaces' }
107
- $workspaceRoot = [System.IO.Path]::GetFullPath($workspaceRoot)
108
- if (-not (Test-Path -LiteralPath $workspaceRoot -PathType Container)) { Fail "WorkspaceRoot missing: $workspaceRoot" }
109
- $cfg['WorkspaceRoot'] = $workspaceRoot
110
- if ($cfg.ContainsKey('AutonomousSupervisorEnabled')) {
111
- if ($cfg.AutonomousSupervisorEnabled -isnot [bool]) { Fail 'AutonomousSupervisorEnabled must be boolean' }
112
- } else {
113
- $cfg['AutonomousSupervisorEnabled'] = $false
114
- }
115
- $componentNames = @('Bridge','Cloud')
116
- if ([bool]$cfg.AutonomousSupervisorEnabled) { $componentNames += 'Supervisor' }
117
- foreach ($name in $componentNames) {
118
- $spec = $cfg[$name]
119
- if (-not $spec -or -not $spec.ContainsKey('FilePath') -or [string]::IsNullOrWhiteSpace([string]$spec.FilePath)) { Fail "$name FilePath missing" }
120
- if (-not $spec.ContainsKey('Arguments')) { Fail "$name Arguments field missing" }
121
- if (-not $spec.ContainsKey('WorkingDirectory')) { Fail "$name WorkingDirectory field missing" }
122
- if (-not (Test-Path -LiteralPath $spec.FilePath -PathType Leaf)) { Fail "$name executable missing: $($spec.FilePath)" }
123
- if (-not $spec.ContainsKey('FileSha256') -or [string]$spec.FileSha256 -cnotmatch '^[0-9a-f]{64}$') { Fail "$name FileSha256 invalid" }
124
- $componentExecutableHash = Get-ExecutableHash ([string]$spec.FilePath)
125
- if ($componentExecutableHash -ne [string]$spec.FileSha256) { Fail "$name executable hash mismatch: expected $($spec.FileSha256), found $componentExecutableHash" }
126
- if ([string]::IsNullOrWhiteSpace([string]$spec.WorkingDirectory) -or -not (Test-Path -LiteralPath $spec.WorkingDirectory -PathType Container)) {
127
- Fail "$name WorkingDirectory missing: $($spec.WorkingDirectory)"
128
- }
129
- Normalize-ComponentDependencies $name $spec
130
- if ($name -eq 'Cloud' -and $spec.ContainsKey('Transport')) {
131
- $transport = [string]$spec.Transport
132
- $dependencyCount = @($spec.Dependencies).Count
133
- if ($transport -eq 'deadbyte.named-tunnel.v1') {
134
- if ($dependencyCount -ne 3) { Fail 'deadbyte.named-tunnel.v1 requires exactly three pinned dependencies' }
135
- } elseif ($transport -eq 'deadbyte.named-tunnel.v2') {
136
- if ($dependencyCount -ne 2) { Fail 'deadbyte.named-tunnel.v2 requires exactly cloudflared plus token-file pins' }
137
- } else { Fail 'Cloud Transport unsupported' }
138
- }
139
- if ($name -in @('Bridge','Supervisor')) {
140
- if (-not $spec.ContainsKey('ScriptPath') -or [string]::IsNullOrWhiteSpace([string]$spec.ScriptPath)) { Fail "$name ScriptPath missing" }
141
- if (-not $spec.ContainsKey('ScriptSha256') -or [string]$spec.ScriptSha256 -cnotmatch '^[0-9a-f]{64}$') { Fail "$name ScriptSha256 invalid" }
142
- $scriptPath = [System.IO.Path]::GetFullPath([string]$spec.ScriptPath)
143
- if (-not (Test-Path -LiteralPath $scriptPath -PathType Leaf)) { Fail "$name ScriptPath missing: $scriptPath" }
144
- if ($name -eq 'Supervisor' -and [System.IO.Path]::GetFileName($scriptPath) -ne 'autonomous-supervisor-host.mjs') { Fail 'Supervisor ScriptPath must target autonomous-supervisor-host.mjs' }
145
- $scriptHash = Get-ExecutableHash $scriptPath
146
- if ($scriptHash -ne [string]$spec.ScriptSha256) { Fail "$name script hash mismatch: expected $($spec.ScriptSha256), found $scriptHash" }
147
- $spec['ScriptPath'] = $scriptPath
148
- }
149
- }
150
- $cloudTransport = if ($cfg.Cloud.ContainsKey('Transport')) { [string]$cfg.Cloud.Transport } else { '' }
151
- if ($cloudTransport -eq 'deadbyte.named-tunnel.v2') {
152
- if (-not $cfg.ContainsKey('McpUrlFile') -or [string]::IsNullOrWhiteSpace([string]$cfg.McpUrlFile)) { Fail 'deadbyte.named-tunnel.v2 requires McpUrlFile' }
153
- if (-not $cfg.ContainsKey('McpUrlFileSha256') -or [string]$cfg.McpUrlFileSha256 -cnotmatch '^[0-9a-f]{64}$') { Fail 'deadbyte.named-tunnel.v2 requires McpUrlFileSha256' }
154
- $mcpUrlFile = [System.IO.Path]::GetFullPath([string]$cfg.McpUrlFile)
155
- if (-not (Test-Path -LiteralPath $mcpUrlFile -PathType Leaf)) { Fail "McpUrlFile missing: $mcpUrlFile" }
156
- $mcpUrlItem = Get-Item -LiteralPath $mcpUrlFile -Force
157
- if (($mcpUrlItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { Fail 'McpUrlFile must not be a reparse point' }
158
- $mcpUrlHash = Get-ExecutableHash $mcpUrlFile
159
- if ($mcpUrlHash -ne [string]$cfg.McpUrlFileSha256) { Fail "McpUrlFile SHA-256 mismatch: expected $($cfg.McpUrlFileSha256), found $mcpUrlHash" }
160
- $cfg['McpUrlFile'] = $mcpUrlFile
161
- }
162
- if ($cfg.ContainsKey('HostPolicyFile') -and -not [string]::IsNullOrWhiteSpace([string]$cfg.HostPolicyFile)) {
163
- $hostPolicy = [System.IO.Path]::GetFullPath([string]$cfg.HostPolicyFile)
164
- if (-not (Test-Path -LiteralPath $hostPolicy -PathType Leaf)) { Fail "HostPolicyFile missing: $hostPolicy" }
165
- $cfg['HostPolicyFile'] = $hostPolicy
166
- } else {
167
- $cfg['HostPolicyFile'] = ''
168
- }
169
- if ($cfg.ContainsKey('MachineRuntimeEnabled')) {
170
- if ($cfg.MachineRuntimeEnabled -isnot [bool]) { Fail 'MachineRuntimeEnabled must be boolean' }
171
- } else {
172
- $cfg['MachineRuntimeEnabled'] = $true
173
- }
174
- if ($cfg.ContainsKey('AgentRuntimeEnabled')) {
175
- if ($cfg.AgentRuntimeEnabled -isnot [bool]) { Fail 'AgentRuntimeEnabled must be boolean' }
176
- } else {
177
- $cfg['AgentRuntimeEnabled'] = $false
178
- }
179
- if ($cfg.ContainsKey('ProcessRuntimeEnabled')) {
180
- if ($cfg.ProcessRuntimeEnabled -isnot [bool]) { Fail 'ProcessRuntimeEnabled must be boolean' }
181
- } else {
182
- $cfg['ProcessRuntimeEnabled'] = $false
183
- }
184
- if ($cfg.ContainsKey('ProcessPolicyFile') -and -not [string]::IsNullOrWhiteSpace([string]$cfg.ProcessPolicyFile)) {
185
- $processPolicy = [System.IO.Path]::GetFullPath([string]$cfg.ProcessPolicyFile)
186
- if (-not (Test-Path -LiteralPath $processPolicy -PathType Leaf)) { Fail "ProcessPolicyFile missing: $processPolicy" }
187
- $cfg['ProcessPolicyFile'] = $processPolicy
188
- } else {
189
- $cfg['ProcessPolicyFile'] = ''
190
- }
1
+ #requires -Version 5.1
2
+ [CmdletBinding()]
3
+ param(
4
+ [Parameter(Position=0)]
5
+ [ValidateSet('menu','start','resume','park','stop','restart','status','url','test','fulltest','toggle','clean','machinetest','armprocess','disarmprocess','processstatus','armhost','disarmhost','hoststatus','hosttest','armcoding','disarmcoding','codingstatus','codingtest','armautonomous','disarmautonomous','autonomousstatus','autonomoustest','supervisorstart','supervisorstop','supervisorstatus','supervisorrecover','armdesktop','disarmdesktop','desktopstatus')]
6
+ [string]$Command = 'menu',
7
+ [switch]$Json
8
+ )
9
+
10
+ Set-StrictMode -Version 3.0
11
+ $ErrorActionPreference = 'Stop'
12
+
13
+ $ControllerSchema = 'deadbyte.controller.v1'
14
+ $Root = Split-Path -Parent $MyInvocation.MyCommand.Path
15
+ $RuntimeDir = if ($env:DEADBYTE_CONTROLLER_RUNTIME_ROOT) { [System.IO.Path]::GetFullPath([string]$env:DEADBYTE_CONTROLLER_RUNTIME_ROOT) } else { Join-Path $Root 'runtime' }
16
+ $LogDir = Join-Path $RuntimeDir 'logs'
17
+ $ConfigPath = if ($env:DEADBYTE_CONTROLLER_CONFIG) { $env:DEADBYTE_CONTROLLER_CONFIG } else { Join-Path $Root 'deadbyte-controller.config.psd1' }
18
+
19
+ function Fail([string]$Message) { throw "DEADBYTE controller: $Message" }
20
+ if ($Json -and $Command -ne 'status') { Fail 'JSON observation is available only for status' }
21
+
22
+ function Get-ControllerMutationMutexName {
23
+ $normalized = ([System.IO.Path]::GetFullPath($RuntimeDir)).TrimEnd('\\').ToLowerInvariant()
24
+ $sha = [System.Security.Cryptography.SHA256]::Create()
25
+ try {
26
+ $bytes = [System.Text.Encoding]::UTF8.GetBytes($normalized)
27
+ $digest = $sha.ComputeHash($bytes)
28
+ } finally {
29
+ $sha.Dispose()
30
+ }
31
+ $hex = ([System.BitConverter]::ToString($digest)).Replace('-', '').ToLowerInvariant().Substring(0, 24)
32
+ return "Local\DEADBYTE-MCP-Controller-$hex"
33
+ }
34
+
35
+ function Invoke-WithControllerMutationLock([string]$Name, [scriptblock]$Body) {
36
+ $mutexName = Get-ControllerMutationMutexName
37
+ $mutex = New-Object System.Threading.Mutex($false, $mutexName)
38
+ $held = $false
39
+ try {
40
+ try {
41
+ $held = $mutex.WaitOne([TimeSpan]::FromSeconds(180))
42
+ } catch [System.Threading.AbandonedMutexException] {
43
+ $held = $true
44
+ }
45
+ if (-not $held) { Fail "controller mutation lock timeout for '$Name'" }
46
+ & $Body
47
+ } finally {
48
+ if ($held) {
49
+ try { $mutex.ReleaseMutex() } catch {}
50
+ }
51
+ $mutex.Dispose()
52
+ }
53
+ }
54
+
55
+ function Ensure-Directory([string]$Path) { if (-not (Test-Path -LiteralPath $Path -PathType Container)) { New-Item -ItemType Directory -Path $Path -Force | Out-Null } }
56
+ function Ledger-Path([string]$Name) { Join-Path $RuntimeDir ("{0}.instance.json" -f $Name.ToLowerInvariant()) }
57
+ function Log-Path([string]$Name, [string]$Kind) { Join-Path $LogDir ("{0}.{1}.log" -f $Name.ToLowerInvariant(), $Kind) }
58
+ function Controller-StartFailurePath { Join-Path $RuntimeDir 'controller-start-failure.json' }
59
+ function Read-LogTailBounded([string]$Path, [int]$MaxChars = 8192) {
60
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return '' }
61
+ try {
62
+ $text = (@(Get-Content -LiteralPath $Path -Encoding UTF8 -Tail 80 -ErrorAction Stop) -join [Environment]::NewLine)
63
+ if ($text.Length -gt $MaxChars) { return $text.Substring($text.Length - $MaxChars) }
64
+ return $text
65
+ } catch { return '' }
66
+ }
67
+ function Write-ControllerStartFailure([string]$Name, [System.Exception]$Exception) {
68
+ Ensure-Directory $RuntimeDir; Ensure-Directory $LogDir
69
+ $body = [ordered]@{
70
+ schema = 'deadbyte.controller-start-failure.v1'
71
+ command = $Name
72
+ failed_at_utc = [DateTime]::UtcNow.ToString('o')
73
+ message = [string]$Exception.Message
74
+ bridge_stdout_tail = Read-LogTailBounded (Log-Path 'Bridge' 'stdout')
75
+ bridge_stderr_tail = Read-LogTailBounded (Log-Path 'Bridge' 'stderr')
76
+ cloud_stdout_tail = Read-LogTailBounded (Log-Path 'Cloud' 'stdout')
77
+ cloud_stderr_tail = Read-LogTailBounded (Log-Path 'Cloud' 'stderr')
78
+ }
79
+ $path = Controller-StartFailurePath
80
+ $tmp = $path + '.tmp'
81
+ $json = $body | ConvertTo-Json -Depth 4
82
+ [System.IO.File]::WriteAllText($tmp, $json, (New-Object System.Text.UTF8Encoding($false)))
83
+ Move-Item -LiteralPath $tmp -Destination $path -Force
84
+ }
85
+
86
+
87
+ function Read-TextFileSafe([string]$Path) {
88
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return '' }
89
+ $text = Get-Content -LiteralPath $Path -Raw -Encoding UTF8
90
+ if ($null -eq $text) { return '' }
91
+ return [string]$text
92
+ }
93
+
94
+ function Get-Config {
95
+ if (-not (Test-Path -LiteralPath $ConfigPath -PathType Leaf)) {
96
+ Fail "missing trusted config '$ConfigPath'; copy deadbyte-controller.config.psd1.example and set exact long-lived executable paths"
97
+ }
98
+ $cfg = Import-PowerShellDataFile -LiteralPath $ConfigPath
99
+ if ($cfg.Schema -ne $ControllerSchema) { Fail 'controller config schema mismatch' }
100
+ $coreRoot = if ($cfg.ContainsKey('CoreRoot') -and -not [string]::IsNullOrWhiteSpace([string]$cfg.CoreRoot)) { [string]$cfg.CoreRoot } else { Split-Path -Parent $Root }
101
+ $coreRoot = [System.IO.Path]::GetFullPath($coreRoot)
102
+ if (-not (Test-Path -LiteralPath (Join-Path $coreRoot 'package.json') -PathType Leaf) -or -not (Test-Path -LiteralPath (Join-Path $coreRoot 'src\mcp-server.mjs') -PathType Leaf)) {
103
+ Fail "CoreRoot is not a DEADBYTE MCP package: $coreRoot"
104
+ }
105
+ $cfg['CoreRoot'] = $coreRoot
106
+ $workspaceRoot = if ($cfg.ContainsKey('WorkspaceRoot') -and -not [string]::IsNullOrWhiteSpace([string]$cfg.WorkspaceRoot)) { [string]$cfg.WorkspaceRoot } else { Join-Path $coreRoot 'workspaces' }
107
+ $workspaceRoot = [System.IO.Path]::GetFullPath($workspaceRoot)
108
+ if (-not (Test-Path -LiteralPath $workspaceRoot -PathType Container)) { Fail "WorkspaceRoot missing: $workspaceRoot" }
109
+ $cfg['WorkspaceRoot'] = $workspaceRoot
110
+ if ($cfg.ContainsKey('AutonomousSupervisorEnabled')) {
111
+ if ($cfg.AutonomousSupervisorEnabled -isnot [bool]) { Fail 'AutonomousSupervisorEnabled must be boolean' }
112
+ } else {
113
+ $cfg['AutonomousSupervisorEnabled'] = $false
114
+ }
115
+ $componentNames = @('Bridge','Cloud')
116
+ if ([bool]$cfg.AutonomousSupervisorEnabled) { $componentNames += 'Supervisor' }
117
+ foreach ($name in $componentNames) {
118
+ $spec = $cfg[$name]
119
+ if (-not $spec -or -not $spec.ContainsKey('FilePath') -or [string]::IsNullOrWhiteSpace([string]$spec.FilePath)) { Fail "$name FilePath missing" }
120
+ if (-not $spec.ContainsKey('Arguments')) { Fail "$name Arguments field missing" }
121
+ if (-not $spec.ContainsKey('WorkingDirectory')) { Fail "$name WorkingDirectory field missing" }
122
+ if (-not (Test-Path -LiteralPath $spec.FilePath -PathType Leaf)) { Fail "$name executable missing: $($spec.FilePath)" }
123
+ if (-not $spec.ContainsKey('FileSha256') -or [string]$spec.FileSha256 -cnotmatch '^[0-9a-f]{64}$') { Fail "$name FileSha256 invalid" }
124
+ $componentExecutableHash = Get-ExecutableHash ([string]$spec.FilePath)
125
+ if ($componentExecutableHash -ne [string]$spec.FileSha256) { Fail "$name executable hash mismatch: expected $($spec.FileSha256), found $componentExecutableHash" }
126
+ if ([string]::IsNullOrWhiteSpace([string]$spec.WorkingDirectory) -or -not (Test-Path -LiteralPath $spec.WorkingDirectory -PathType Container)) {
127
+ Fail "$name WorkingDirectory missing: $($spec.WorkingDirectory)"
128
+ }
129
+ Normalize-ComponentDependencies $name $spec
130
+ if ($name -eq 'Cloud' -and $spec.ContainsKey('Transport')) {
131
+ $transport = [string]$spec.Transport
132
+ $dependencyCount = @($spec.Dependencies).Count
133
+ if ($transport -eq 'deadbyte.named-tunnel.v1') {
134
+ if ($dependencyCount -ne 3) { Fail 'deadbyte.named-tunnel.v1 requires exactly three pinned dependencies' }
135
+ } elseif ($transport -eq 'deadbyte.named-tunnel.v2') {
136
+ if ($dependencyCount -ne 2) { Fail 'deadbyte.named-tunnel.v2 requires exactly cloudflared plus token-file pins' }
137
+ } else { Fail 'Cloud Transport unsupported' }
138
+ }
139
+ if ($name -in @('Bridge','Supervisor')) {
140
+ if (-not $spec.ContainsKey('ScriptPath') -or [string]::IsNullOrWhiteSpace([string]$spec.ScriptPath)) { Fail "$name ScriptPath missing" }
141
+ if (-not $spec.ContainsKey('ScriptSha256') -or [string]$spec.ScriptSha256 -cnotmatch '^[0-9a-f]{64}$') { Fail "$name ScriptSha256 invalid" }
142
+ $scriptPath = [System.IO.Path]::GetFullPath([string]$spec.ScriptPath)
143
+ if (-not (Test-Path -LiteralPath $scriptPath -PathType Leaf)) { Fail "$name ScriptPath missing: $scriptPath" }
144
+ if ($name -eq 'Supervisor' -and [System.IO.Path]::GetFileName($scriptPath) -ne 'autonomous-supervisor-host.mjs') { Fail 'Supervisor ScriptPath must target autonomous-supervisor-host.mjs' }
145
+ $scriptHash = Get-ExecutableHash $scriptPath
146
+ if ($scriptHash -ne [string]$spec.ScriptSha256) { Fail "$name script hash mismatch: expected $($spec.ScriptSha256), found $scriptHash" }
147
+ $spec['ScriptPath'] = $scriptPath
148
+ }
149
+ }
150
+ $cloudTransport = if ($cfg.Cloud.ContainsKey('Transport')) { [string]$cfg.Cloud.Transport } else { '' }
151
+ if ($cloudTransport -eq 'deadbyte.named-tunnel.v2') {
152
+ if (-not $cfg.ContainsKey('McpUrlFile') -or [string]::IsNullOrWhiteSpace([string]$cfg.McpUrlFile)) { Fail 'deadbyte.named-tunnel.v2 requires McpUrlFile' }
153
+ if (-not $cfg.ContainsKey('McpUrlFileSha256') -or [string]$cfg.McpUrlFileSha256 -cnotmatch '^[0-9a-f]{64}$') { Fail 'deadbyte.named-tunnel.v2 requires McpUrlFileSha256' }
154
+ $mcpUrlFile = [System.IO.Path]::GetFullPath([string]$cfg.McpUrlFile)
155
+ if (-not (Test-Path -LiteralPath $mcpUrlFile -PathType Leaf)) { Fail "McpUrlFile missing: $mcpUrlFile" }
156
+ $mcpUrlItem = Get-Item -LiteralPath $mcpUrlFile -Force
157
+ if (($mcpUrlItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { Fail 'McpUrlFile must not be a reparse point' }
158
+ $mcpUrlHash = Get-ExecutableHash $mcpUrlFile
159
+ if ($mcpUrlHash -ne [string]$cfg.McpUrlFileSha256) { Fail "McpUrlFile SHA-256 mismatch: expected $($cfg.McpUrlFileSha256), found $mcpUrlHash" }
160
+ $cfg['McpUrlFile'] = $mcpUrlFile
161
+ }
162
+ if ($cfg.ContainsKey('HostPolicyFile') -and -not [string]::IsNullOrWhiteSpace([string]$cfg.HostPolicyFile)) {
163
+ $hostPolicy = [System.IO.Path]::GetFullPath([string]$cfg.HostPolicyFile)
164
+ if (-not (Test-Path -LiteralPath $hostPolicy -PathType Leaf)) { Fail "HostPolicyFile missing: $hostPolicy" }
165
+ $cfg['HostPolicyFile'] = $hostPolicy
166
+ } else {
167
+ $cfg['HostPolicyFile'] = ''
168
+ }
169
+ if ($cfg.ContainsKey('MachineRuntimeEnabled')) {
170
+ if ($cfg.MachineRuntimeEnabled -isnot [bool]) { Fail 'MachineRuntimeEnabled must be boolean' }
171
+ } else {
172
+ $cfg['MachineRuntimeEnabled'] = $true
173
+ }
174
+ if ($cfg.ContainsKey('AgentRuntimeEnabled')) {
175
+ if ($cfg.AgentRuntimeEnabled -isnot [bool]) { Fail 'AgentRuntimeEnabled must be boolean' }
176
+ } else {
177
+ $cfg['AgentRuntimeEnabled'] = $false
178
+ }
179
+ if ($cfg.ContainsKey('ProcessRuntimeEnabled')) {
180
+ if ($cfg.ProcessRuntimeEnabled -isnot [bool]) { Fail 'ProcessRuntimeEnabled must be boolean' }
181
+ } else {
182
+ $cfg['ProcessRuntimeEnabled'] = $false
183
+ }
184
+ if ($cfg.ContainsKey('ProcessPolicyFile') -and -not [string]::IsNullOrWhiteSpace([string]$cfg.ProcessPolicyFile)) {
185
+ $processPolicy = [System.IO.Path]::GetFullPath([string]$cfg.ProcessPolicyFile)
186
+ if (-not (Test-Path -LiteralPath $processPolicy -PathType Leaf)) { Fail "ProcessPolicyFile missing: $processPolicy" }
187
+ $cfg['ProcessPolicyFile'] = $processPolicy
188
+ } else {
189
+ $cfg['ProcessPolicyFile'] = ''
190
+ }
191
191
  if ([bool]$cfg.ProcessRuntimeEnabled -and [string]::IsNullOrWhiteSpace([string]$cfg.ProcessPolicyFile)) { Fail 'ProcessRuntimeEnabled requires ProcessPolicyFile' }
192
- if ($cfg.ContainsKey('CodingRuntimeEnabled')) {
193
- if ($cfg.CodingRuntimeEnabled -isnot [bool]) { Fail 'CodingRuntimeEnabled must be boolean' }
194
- } else {
195
- $cfg['CodingRuntimeEnabled'] = $false
196
- }
197
- if ($cfg.ContainsKey('CodingPolicyFile') -and -not [string]::IsNullOrWhiteSpace([string]$cfg.CodingPolicyFile)) {
198
- $codingPolicy = [System.IO.Path]::GetFullPath([string]$cfg.CodingPolicyFile)
199
- if (-not (Test-Path -LiteralPath $codingPolicy -PathType Leaf)) { Fail "CodingPolicyFile missing: $codingPolicy" }
200
- $cfg['CodingPolicyFile'] = $codingPolicy
201
- } else {
202
- $cfg['CodingPolicyFile'] = ''
203
- }
204
- if ([bool]$cfg.CodingRuntimeEnabled -and [string]::IsNullOrWhiteSpace([string]$cfg.CodingPolicyFile)) { Fail 'CodingRuntimeEnabled requires CodingPolicyFile' }
205
- $semanticConfigFile = if ($cfg.ContainsKey('SemanticLspConfigFile')) { [string]$cfg.SemanticLspConfigFile } else { '' }
206
- $semanticConfigSha = if ($cfg.ContainsKey('SemanticLspConfigSha256')) { ([string]$cfg.SemanticLspConfigSha256).ToLowerInvariant() } else { '' }
207
- if ([string]::IsNullOrWhiteSpace($semanticConfigFile) -ne [string]::IsNullOrWhiteSpace($semanticConfigSha)) { Fail 'SemanticLspConfigFile and SemanticLspConfigSha256 must be supplied together' }
208
- if (-not [string]::IsNullOrWhiteSpace($semanticConfigFile)) {
209
- if (-not [bool]$cfg.CodingRuntimeEnabled) { Fail 'SemanticLspConfigFile requires CodingRuntimeEnabled' }
210
- if ($semanticConfigSha -cnotmatch '^[0-9a-f]{64}$') { Fail 'SemanticLspConfigSha256 invalid' }
211
- $semanticConfigFile = [System.IO.Path]::GetFullPath($semanticConfigFile)
212
- if (-not (Test-Path -LiteralPath $semanticConfigFile -PathType Leaf)) { Fail "SemanticLspConfigFile missing: $semanticConfigFile" }
213
- $semanticItem = Get-Item -LiteralPath $semanticConfigFile -Force
214
- if (($semanticItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { Fail 'SemanticLspConfigFile must not be a reparse point' }
215
- $semanticObserved = Get-ExecutableHash $semanticConfigFile
216
- if ($semanticObserved -ne $semanticConfigSha) { Fail "semantic LSP config SHA-256 mismatch: expected $semanticConfigSha, found $semanticObserved" }
217
- $cfg['SemanticLspConfigFile'] = $semanticConfigFile
218
- $cfg['SemanticLspConfigSha256'] = $semanticConfigSha
219
- } else {
220
- $cfg['SemanticLspConfigFile'] = ''
221
- $cfg['SemanticLspConfigSha256'] = ''
222
- }
223
- if ($cfg.ContainsKey('MachineFsRuntimeEnabled')) {
224
- if ($cfg.MachineFsRuntimeEnabled -isnot [bool]) { Fail 'MachineFsRuntimeEnabled must be boolean' }
225
- } else {
226
- $cfg['MachineFsRuntimeEnabled'] = $false
227
- }
228
- if ([bool]$cfg.MachineFsRuntimeEnabled -and -not [bool]$cfg.CodingRuntimeEnabled) { Fail 'MachineFsRuntimeEnabled requires CodingRuntimeEnabled' }
229
- if ($cfg.ContainsKey('ObservationRuntimeEnabled')) {
230
- if ($cfg.ObservationRuntimeEnabled -isnot [bool]) { Fail 'ObservationRuntimeEnabled must be boolean' }
231
- } else {
232
- $cfg['ObservationRuntimeEnabled'] = $false
233
- }
234
- if ([bool]$cfg.ObservationRuntimeEnabled -and -not [bool]$cfg.MachineRuntimeEnabled) { Fail 'ObservationRuntimeEnabled requires MachineRuntimeEnabled' }
235
- if ([bool]$cfg.ObservationRuntimeEnabled -and -not [bool]$cfg.CodingRuntimeEnabled) { Fail 'ObservationRuntimeEnabled requires CodingRuntimeEnabled' }
236
- if ($cfg.ContainsKey('DesktopRuntimeEnabled')) {
237
- if ($cfg.DesktopRuntimeEnabled -isnot [bool]) { Fail 'DesktopRuntimeEnabled must be boolean' }
238
- } else {
239
- $cfg['DesktopRuntimeEnabled'] = $false
240
- }
241
- if ($cfg.ContainsKey('DesktopPolicyFile') -and -not [string]::IsNullOrWhiteSpace([string]$cfg.DesktopPolicyFile)) {
242
- $desktopPolicy = [System.IO.Path]::GetFullPath([string]$cfg.DesktopPolicyFile)
243
- if (-not (Test-Path -LiteralPath $desktopPolicy -PathType Leaf)) { Fail "DesktopPolicyFile missing: $desktopPolicy" }
244
- $cfg['DesktopPolicyFile'] = $desktopPolicy
245
- } else {
246
- $cfg['DesktopPolicyFile'] = ''
247
- }
248
- if ([bool]$cfg.DesktopRuntimeEnabled -and [string]::IsNullOrWhiteSpace([string]$cfg.DesktopPolicyFile)) { Fail 'DesktopRuntimeEnabled requires DesktopPolicyFile' }
249
- if ($cfg.ContainsKey('AutonomousRuntimeEnabled')) {
250
- if ($cfg.AutonomousRuntimeEnabled -isnot [bool]) { Fail 'AutonomousRuntimeEnabled must be boolean' }
251
- } else {
252
- $cfg['AutonomousRuntimeEnabled'] = $false
253
- }
254
- if ($cfg.ContainsKey('AutonomousPolicyFile') -and -not [string]::IsNullOrWhiteSpace([string]$cfg.AutonomousPolicyFile)) {
255
- $autonomousPolicy = [System.IO.Path]::GetFullPath([string]$cfg.AutonomousPolicyFile)
256
- if (-not (Test-Path -LiteralPath $autonomousPolicy -PathType Leaf)) { Fail "AutonomousPolicyFile missing: $autonomousPolicy" }
257
- $cfg['AutonomousPolicyFile'] = $autonomousPolicy
258
- } else {
259
- $cfg['AutonomousPolicyFile'] = ''
260
- }
261
- if ([bool]$cfg.AutonomousRuntimeEnabled -and -not [bool]$cfg.CodingRuntimeEnabled) { Fail 'AutonomousRuntimeEnabled requires CodingRuntimeEnabled' }
262
- if ([bool]$cfg.AutonomousRuntimeEnabled -and [string]::IsNullOrWhiteSpace([string]$cfg.AutonomousPolicyFile)) { Fail 'AutonomousRuntimeEnabled requires AutonomousPolicyFile' }
263
- if ([bool]$cfg.AutonomousSupervisorEnabled -and -not [bool]$cfg.AutonomousRuntimeEnabled) { Fail 'AutonomousSupervisorEnabled requires AutonomousRuntimeEnabled' }
264
- if ([int]$cfg.StartTimeoutSeconds -lt 1 -or [int]$cfg.StartTimeoutSeconds -gt 120) { Fail 'StartTimeoutSeconds out of range' }
265
- if ([int]$cfg.ProbeTimeoutSeconds -lt 1 -or [int]$cfg.ProbeTimeoutSeconds -gt 15) { Fail 'ProbeTimeoutSeconds out of range' }
266
- return $cfg
267
- }
268
-
269
- function Assert-ControllerRootBound($Cfg) {
270
- $expected = [System.IO.Path]::GetFullPath((Join-Path ([string]$Cfg.CoreRoot) 'controller')).TrimEnd('\')
271
- $actual = [System.IO.Path]::GetFullPath($Root).TrimEnd('\')
272
- if (-not [string]::Equals($actual, $expected, [System.StringComparison]::OrdinalIgnoreCase)) {
273
- Fail "controller root mismatch: running '$actual', expected '$expected'; use canonical DEADBYTE-MCP.cmd"
274
- }
275
- }
276
-
277
- function Get-ExecutableHash([string]$Path) {
278
- (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
279
- }
280
-
281
- function Normalize-ComponentDependencies([string]$Name, $Spec) {
282
- if (-not $Spec.ContainsKey('Dependencies')) { $Spec['Dependencies'] = @(); return }
283
- $dependencies = @($Spec.Dependencies)
284
- if ($dependencies.Count -gt 16) { Fail "$Name dependency count exceeds 16" }
285
- $normalized = @()
286
- foreach ($dependency in $dependencies) {
287
- $depPath = [System.IO.Path]::GetFullPath([string]$dependency.Path)
288
- $depHash = ([string]$dependency.Sha256).ToLowerInvariant()
289
- if ([string]::IsNullOrWhiteSpace($depPath) -or -not (Test-Path -LiteralPath $depPath -PathType Leaf)) { Fail "$Name dependency missing: $depPath" }
290
- if ($depHash -cnotmatch '^[0-9a-f]{64}$') { Fail "$Name dependency SHA-256 invalid: $depPath" }
291
- $item = Get-Item -LiteralPath $depPath -Force
292
- if (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { Fail "$Name dependency must not be a reparse point: $depPath" }
293
- $actual = Get-ExecutableHash $depPath
294
- if ($actual -ne $depHash) { Fail "$Name dependency hash mismatch in trusted config: $depPath" }
295
- $depName = if ([string]::IsNullOrWhiteSpace([string]$dependency.Name)) { [System.IO.Path]::GetFileName($depPath) } else { [string]$dependency.Name }
296
- $normalized += [ordered]@{ Name=$depName; Path=$depPath; Sha256=$depHash }
297
- }
298
- $Spec['Dependencies'] = $normalized
299
- }
300
-
301
- function Assert-ComponentDependencies([string]$Name, $Spec, [string]$Phase) {
302
- foreach ($dependency in @($Spec.Dependencies)) {
303
- $depPath = [string]$dependency.Path
304
- $expected = [string]$dependency.Sha256
305
- if (-not (Test-Path -LiteralPath $depPath -PathType Leaf)) { Fail "$Name dependency missing ${Phase}: $depPath" }
306
- $actual = Get-ExecutableHash $depPath
307
- if ($actual -ne $expected) { Fail "$Name dependency hash mismatch ${Phase}: $depPath expected $expected found $actual" }
308
- }
309
- }
310
-
311
- function Get-LimitedProcessImagePath([int]$ProcessId) {
312
- if (-not ('DeadbyteProcessQuery' -as [type])) {
313
- $source = @'
314
- using System;
315
- using System.Text;
316
- using System.Runtime.InteropServices;
317
- public static class DeadbyteProcessQuery {
318
- [DllImport("kernel32.dll", SetLastError=true)] static extern IntPtr OpenProcess(uint access, bool inheritHandle, int processId);
319
- [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] static extern bool QueryFullProcessImageName(IntPtr process, uint flags, StringBuilder path, ref int size);
320
- [DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
321
- public static string GetImagePath(int processId) { const uint PROCESS_QUERY_LIMITED_INFORMATION=0x1000; IntPtr h=OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,false,processId); if(h==IntPtr.Zero)return null; try { var b=new StringBuilder(32768); int n=b.Capacity; return QueryFullProcessImageName(h,0,b,ref n)?b.ToString():null; } finally { CloseHandle(h); } }
322
- }
323
- '@
324
- Add-Type -TypeDefinition $source -Language CSharp -ErrorAction Stop
325
- }
326
- try { $path=[DeadbyteProcessQuery]::GetImagePath($ProcessId); if ($null -eq $path) { return '' }; return [string]$path } catch { return '' }
327
- }
328
-
329
- function Get-ProcessObservation([int]$ProcessId) {
330
- try { $p = Get-Process -Id $ProcessId -ErrorAction Stop } catch { return $null }
331
- $image = ''
332
- try {
333
- $cim = Get-CimInstance Win32_Process -Filter ("ProcessId={0}" -f $ProcessId) -ErrorAction Stop
334
- if ($cim -and -not [string]::IsNullOrWhiteSpace([string]$cim.ExecutablePath)) {
335
- $image = [string]$cim.ExecutablePath
336
- }
337
- } catch {}
338
- if ([string]::IsNullOrWhiteSpace($image)) {
339
- try { $image = [string]$p.Path } catch {}
340
- }
341
- if ([string]::IsNullOrWhiteSpace($image)) {
342
- try { $image = Get-LimitedProcessImagePath $ProcessId } catch {}
343
- }
344
- if ([string]::IsNullOrWhiteSpace($image)) {
345
- try { $image = [string]$p.MainModule.FileName } catch {}
346
- }
347
- if ([string]::IsNullOrWhiteSpace($image)) { return $null }
348
- try { $image = [System.IO.Path]::GetFullPath($image) } catch { return $null }
349
- if (-not (Test-Path -LiteralPath $image -PathType Leaf)) { return $null }
350
- try { $imageHash = Get-ExecutableHash $image } catch { return $null }
351
- [pscustomobject]@{
352
- pid = [int]$p.Id
353
- creation_time_utc = $p.StartTime.ToUniversalTime().ToString('o')
354
- image_path = $image
355
- image_sha256 = $imageHash
356
- }
357
- }
358
-
359
- function Read-Ledger([string]$Name) {
360
- $path = Ledger-Path $Name
361
- if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { return $null }
362
- try { return (Get-Content -LiteralPath $path -Raw -Encoding UTF8 | ConvertFrom-Json) } catch { return [pscustomobject]@{ corrupt = $true; path = $path } }
363
- }
364
-
365
- function Test-OwnedInstance([string]$Name) {
366
- $ledger = Read-Ledger $Name
367
- if (-not $ledger) { return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='no-ledger'; Ledger=$null; Observation=$null } }
368
- if ($ledger.PSObject.Properties.Name -contains 'corrupt' -and $ledger.corrupt -eq $true) { return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='corrupt-ledger'; Ledger=$ledger; Observation=$null } }
369
- if ($ledger.schema -ne $ControllerSchema -or -not $ledger.instance_nonce) { return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='ledger-schema'; Ledger=$ledger; Observation=$null } }
370
- if ($Name -in @('Bridge','Supervisor')) {
371
- if (-not ($ledger.PSObject.Properties.Name -contains 'script_path') -or -not ($ledger.PSObject.Properties.Name -contains 'script_sha256')) {
372
- return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='ledger-schema'; Ledger=$ledger; Observation=$null }
373
- }
374
- $scriptPath = [string]$ledger.script_path
375
- $scriptHash = [string]$ledger.script_sha256
376
- if ([string]::IsNullOrWhiteSpace($scriptPath) -or $scriptHash -cnotmatch '^[0-9a-f]{64}$' -or -not (Test-Path -LiteralPath $scriptPath -PathType Leaf)) {
377
- return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='script-hash-mismatch'; Ledger=$ledger; Observation=$null }
378
- }
379
- try { $observedScriptHash = Get-ExecutableHash $scriptPath } catch { $observedScriptHash = '' }
380
- if ($observedScriptHash -ne $scriptHash) { return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='script-hash-mismatch'; Ledger=$ledger; Observation=$null } }
381
- }
382
- if ($ledger.PSObject.Properties.Name -contains 'dependencies') {
383
- foreach ($dependency in @($ledger.dependencies)) {
384
- $depPath = [string]$dependency.path
385
- $depHash = [string]$dependency.sha256
386
- if ([string]::IsNullOrWhiteSpace($depPath) -or $depHash -cnotmatch '^[0-9a-f]{64}$' -or -not (Test-Path -LiteralPath $depPath -PathType Leaf)) {
387
- return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='dependency-hash-mismatch'; Ledger=$ledger; Observation=$null }
388
- }
389
- try { $observedDependencyHash = Get-ExecutableHash $depPath } catch { $observedDependencyHash = '' }
390
- if ($observedDependencyHash -ne $depHash) { return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='dependency-hash-mismatch'; Ledger=$ledger; Observation=$null } }
391
- }
392
- }
393
- $obs = Get-ProcessObservation ([int]$ledger.pid)
394
- if (-not $obs) { try { [void](Get-Process -Id ([int]$ledger.pid) -ErrorAction Stop); return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='process-unobservable'; Ledger=$ledger; Observation=$null } } catch { return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='process-missing'; Ledger=$ledger; Observation=$null } } }
395
- if ($obs.creation_time_utc -ne [string]$ledger.creation_time_utc) { return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='pid-reused'; Ledger=$ledger; Observation=$obs } }
396
- if (-not [string]::Equals($obs.image_path, [string]$ledger.image_path, [System.StringComparison]::OrdinalIgnoreCase)) { return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='image-path-mismatch'; Ledger=$ledger; Observation=$obs } }
397
- if ($obs.image_sha256 -ne [string]$ledger.image_sha256) { return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='image-hash-mismatch'; Ledger=$ledger; Observation=$obs } }
398
- [pscustomobject]@{ Name=$Name; Alive=$true; Reason='verified'; Ledger=$ledger; Observation=$obs }
399
- }
400
-
401
- function Reconcile-PidReusedLedger([string]$Name, $Ownership) {
402
- if ($null -eq $Ownership -or [string]$Ownership.Reason -ne 'pid-reused') { Fail "$Name PID-reuse reconcile requires pid-reused ownership state" }
403
- if ($null -eq $Ownership.Ledger -or $null -eq $Ownership.Observation) { Fail "$Name PID-reuse reconcile requires stale ledger and current process observation" }
404
- $ledger = $Ownership.Ledger
405
- $observed = $Ownership.Observation
406
- if ([int]$ledger.pid -ne [int]$observed.pid) { Fail "$Name PID-reuse reconcile PID mismatch" }
407
- if ([string]$ledger.creation_time_utc -eq [string]$observed.creation_time_utc) { Fail "$Name PID-reuse reconcile requires creation-time mismatch" }
408
- $ledgerPath = Ledger-Path $Name
409
- if (-not (Test-Path -LiteralPath $ledgerPath -PathType Leaf)) { Fail "$Name PID-reuse stale ledger disappeared before archive" }
410
- $ledgerSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $ledgerPath).Hash.ToLowerInvariant()
411
- if ($ledgerSha256 -cnotmatch '^[0-9a-f]{64}$') { Fail "$Name PID-reuse stale ledger SHA-256 invalid" }
412
- $evidenceDir = Join-Path $RuntimeDir 'ownership-evidence'
413
- Ensure-Directory $evidenceDir
414
- $archivePath = Join-Path $evidenceDir ("{0}.stale.{1}.json" -f $Name.ToLowerInvariant(), $ledgerSha256)
415
- if (Test-Path -LiteralPath $archivePath -PathType Leaf) {
416
- $existingSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $archivePath).Hash.ToLowerInvariant()
417
- if ($existingSha256 -ne $ledgerSha256) { Fail "$Name PID-reuse evidence archive hash conflict" }
418
- } else {
419
- Copy-Item -LiteralPath $ledgerPath -Destination $archivePath
420
- }
421
- $archiveSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $archivePath).Hash.ToLowerInvariant()
422
- if ($archiveSha256 -ne $ledgerSha256) { Fail "$Name PID-reuse evidence archive verification failed" }
423
- $receipt = [ordered]@{
424
- schema = 'deadbyte.ownership-recovery.v1'
425
- component = $Name.ToLowerInvariant()
426
- reason = 'pid-reused'
427
- stale_ledger_sha256 = $ledgerSha256
428
- stale_ledger_archive = $archivePath
429
- stale_pid = [int]$ledger.pid
430
- stale_creation_time_utc = [string]$ledger.creation_time_utc
431
- stale_image_path = [string]$ledger.image_path
432
- stale_image_sha256 = [string]$ledger.image_sha256
433
- observed_pid = [int]$observed.pid
434
- observed_creation_time_utc = [string]$observed.creation_time_utc
435
- observed_image_path = [string]$observed.image_path
436
- observed_image_sha256 = [string]$observed.image_sha256
437
- reconciled_at_utc = [DateTime]::UtcNow.ToString('o')
438
- action = 'archive-stale-ledger-remove-metadata-no-process-kill'
439
- }
440
- $receiptPath = Join-Path $evidenceDir ("{0}.recovery.{1}.json" -f $Name.ToLowerInvariant(), $ledgerSha256)
441
- $receiptTemp = $receiptPath + '.tmp'
442
- $receiptJson = $receipt | ConvertTo-Json -Depth 5
443
- [System.IO.File]::WriteAllText($receiptTemp, $receiptJson, (New-Object System.Text.UTF8Encoding($false)))
444
- Move-Item -LiteralPath $receiptTemp -Destination $receiptPath -Force
445
- Remove-Item -LiteralPath (Ledger-Path $Name) -Force
446
- if (Test-Path -LiteralPath (Ledger-Path $Name) -PathType Leaf) { Fail "$Name PID-reuse stale ownership metadata removal failed" }
447
- return [pscustomobject]@{ Name=$Name; Reason='pid-reused-reconciled'; LedgerSha256=$ledgerSha256; ArchivePath=$archivePath; ReceiptPath=$receiptPath }
448
- }
449
-
450
- function Write-Ledger([string]$Name, $Observation, [string]$Nonce, $Spec = $null) {
451
- Ensure-Directory $RuntimeDir
452
- $body = [ordered]@{
453
- schema = $ControllerSchema
454
- component = $Name.ToLowerInvariant()
455
- instance_nonce = $Nonce
456
- pid = [int]$Observation.pid
457
- creation_time_utc = [string]$Observation.creation_time_utc
458
- image_path = [string]$Observation.image_path
459
- image_sha256 = [string]$Observation.image_sha256
460
- }
461
- if ($Name -in @('Bridge','Supervisor')) {
462
- if (-not $Spec) { Fail "$Name ledger requires launch specification" }
463
- $body['script_path'] = [string]$Spec.ScriptPath
464
- $body['script_sha256'] = [string]$Spec.ScriptSha256
465
- }
466
- if ($Spec -and @($Spec.Dependencies).Count -gt 0) {
467
- $body['dependencies'] = @($Spec.Dependencies | ForEach-Object { [ordered]@{ name=[string]$_.Name; path=[string]$_.Path; sha256=[string]$_.Sha256 } })
468
- }
469
- $tmp = (Ledger-Path $Name) + '.tmp'
470
- $body | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $tmp -Encoding UTF8
471
- Move-Item -LiteralPath $tmp -Destination (Ledger-Path $Name) -Force
472
- }
473
-
474
- function Start-OwnedComponent([string]$Name, $Spec, $Cfg = $null) {
475
- $current = Test-OwnedInstance $Name
476
- if ($current.Alive) { return $current }
477
- if ($current.Reason -eq 'pid-reused') {
478
- Reconcile-PidReusedLedger $Name $current | Out-Null
479
- $current = Test-OwnedInstance $Name
480
- if ($current.Reason -ne 'no-ledger') { Fail "$Name PID-reuse reconcile did not clear stale ownership metadata" }
481
- }
482
- if ($current.Reason -in @('process-unobservable','image-path-mismatch','image-hash-mismatch','script-hash-mismatch','dependency-hash-mismatch','corrupt-ledger','ledger-schema')) {
483
- Fail "$Name ownership conflict: $($current.Reason); refusing to attach or kill an unverified process"
484
- }
485
- Remove-Item -LiteralPath (Ledger-Path $Name) -Force -ErrorAction SilentlyContinue
486
- if ($Name -eq 'Cloud') { Remove-Item -LiteralPath (Join-Path $RuntimeDir 'current-origin.txt') -Force -ErrorAction SilentlyContinue }
487
- Ensure-Directory $LogDir
488
- $stdout = Log-Path $Name 'stdout'
489
- $stderr = Log-Path $Name 'stderr'
490
- Remove-Item -LiteralPath $stdout,$stderr -Force -ErrorAction SilentlyContinue
491
- $nonceBytes = New-Object byte[] 32
492
- [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($nonceBytes)
493
- $nonce = -join ($nonceBytes | ForEach-Object { $_.ToString('x2') })
494
- $componentExecutableHash = Get-ExecutableHash ([string]$Spec.FilePath)
495
- if ($componentExecutableHash -ne [string]$Spec.FileSha256) { Fail "$Name executable hash mismatch before start: expected $($Spec.FileSha256), found $componentExecutableHash" }
496
- Assert-ComponentDependencies $Name $Spec 'before start'
497
- if ($Name -in @('Bridge','Supervisor')) {
498
- $scriptHash = Get-ExecutableHash ([string]$Spec.ScriptPath)
499
- if ($scriptHash -ne [string]$Spec.ScriptSha256) { Fail "$Name script hash mismatch before start: expected $($Spec.ScriptSha256), found $scriptHash" }
500
- }
501
- $args = [string]$Spec.Arguments
502
- $start = @{ FilePath=[string]$Spec.FilePath; WorkingDirectory=[string]$Spec.WorkingDirectory; PassThru=$true; WindowStyle='Hidden'; RedirectStandardOutput=$stdout; RedirectStandardError=$stderr }
503
- if (-not [string]::IsNullOrWhiteSpace($args)) { $start.ArgumentList = $args }
504
- $hadCoreRootEnv = Test-Path Env:DEADBYTE_CORE_ROOT
505
- $previousCoreRootEnv = $env:DEADBYTE_CORE_ROOT
506
- $hadHostPolicyEnv = Test-Path Env:DEADBYTE_PRIVILEGED_POLICY
507
- $previousHostPolicyEnv = $env:DEADBYTE_PRIVILEGED_POLICY
508
- $hadWorkspaceRootEnv = Test-Path Env:DEADBYTE_WORKSPACE_ROOT
509
- $previousWorkspaceRootEnv = $env:DEADBYTE_WORKSPACE_ROOT
510
- $hadAuditRuntimeEnv = Test-Path Env:DEADBYTE_AUDIT_RUNTIME
511
- $previousAuditRuntimeEnv = $env:DEADBYTE_AUDIT_RUNTIME
512
- $hadMachineRuntimeEnv = Test-Path Env:DEADBYTE_MACHINE_RUNTIME
513
- $previousMachineRuntimeEnv = $env:DEADBYTE_MACHINE_RUNTIME
514
- $hadAgentRuntimeEnv = Test-Path Env:DEADBYTE_AGENT_RUNTIME
515
- $previousAgentRuntimeEnv = $env:DEADBYTE_AGENT_RUNTIME
516
- $hadProcessRuntimeEnv = Test-Path Env:DEADBYTE_PROCESS_RUNTIME
517
- $previousProcessRuntimeEnv = $env:DEADBYTE_PROCESS_RUNTIME
518
- $hadProcessPolicyEnv = Test-Path Env:DEADBYTE_PROCESS_POLICY
519
- $previousProcessPolicyEnv = $env:DEADBYTE_PROCESS_POLICY
520
- $hadCodingRuntimeEnv = Test-Path Env:DEADBYTE_CODING_RUNTIME
521
- $previousCodingRuntimeEnv = $env:DEADBYTE_CODING_RUNTIME
522
- $hadMachineFsRuntimeEnv = Test-Path Env:DEADBYTE_MACHINE_FS_RUNTIME
523
- $previousMachineFsRuntimeEnv = $env:DEADBYTE_MACHINE_FS_RUNTIME
524
- $hadObservationRuntimeEnv = Test-Path Env:DEADBYTE_OBSERVATION_RUNTIME
525
- $previousObservationRuntimeEnv = $env:DEADBYTE_OBSERVATION_RUNTIME
526
- $hadDesktopRuntimeEnv = Test-Path Env:DEADBYTE_DESKTOP_RUNTIME
527
- $previousDesktopRuntimeEnv = $env:DEADBYTE_DESKTOP_RUNTIME
528
- $hadDesktopPolicyEnv = Test-Path Env:DEADBYTE_DESKTOP_POLICY
529
- $previousDesktopPolicyEnv = $env:DEADBYTE_DESKTOP_POLICY
530
- $hadCodingPolicyEnv = Test-Path Env:DEADBYTE_CODING_POLICY
531
- $previousCodingPolicyEnv = $env:DEADBYTE_CODING_POLICY
532
- $hadSemanticConfigEnv = Test-Path Env:DEADBYTE_SEMANTIC_LSP_CONFIG
533
- $previousSemanticLspConfigEnv = $env:DEADBYTE_SEMANTIC_LSP_CONFIG
534
- $hadSemanticConfigShaEnv = Test-Path Env:DEADBYTE_SEMANTIC_LSP_CONFIG_SHA256
535
- $previousSemanticLspConfigShaEnv = $env:DEADBYTE_SEMANTIC_LSP_CONFIG_SHA256
536
- $hadAutonomousRuntimeEnv = Test-Path Env:DEADBYTE_AUTONOMOUS_RUNTIME
537
- $previousAutonomousRuntimeEnv = $env:DEADBYTE_AUTONOMOUS_RUNTIME
538
- $hadAutonomousPolicyEnv = Test-Path Env:DEADBYTE_AUTONOMOUS_POLICY
539
- $previousAutonomousPolicyEnv = $env:DEADBYTE_AUTONOMOUS_POLICY
540
- $hadSupervisorOnceEnv = Test-Path Env:DEADBYTE_SUPERVISOR_ONCE
541
- $previousSupervisorOnceEnv = $env:DEADBYTE_SUPERVISOR_ONCE
542
- $hadSupervisorMarkerEnv = Test-Path Env:DEADBYTE_SUPERVISOR_MARKER
543
- $previousSupervisorMarkerEnv = $env:DEADBYTE_SUPERVISOR_MARKER
544
- $hadAutonomousSupervisorModeEnv = Test-Path Env:DEADBYTE_AUTONOMOUS_SUPERVISOR_MODE
545
- $previousAutonomousSupervisorModeEnv = $env:DEADBYTE_AUTONOMOUS_SUPERVISOR_MODE
546
- $hadCapabilityProfileEnv = Test-Path Env:DEADBYTE_CAPABILITY_PROFILE
547
- $previousCapabilityProfileEnv = $env:DEADBYTE_CAPABILITY_PROFILE
548
- try {
549
- if ($Name -eq 'Bridge' -and $Cfg) {
550
- $env:DEADBYTE_CAPABILITY_PROFILE = 'compact'
551
- $env:DEADBYTE_AUDIT_RUNTIME = '1'
552
- $env:DEADBYTE_CORE_ROOT = [string]$Cfg.CoreRoot
553
- $env:DEADBYTE_WORKSPACE_ROOT = [string]$Cfg.WorkspaceRoot
554
- $env:DEADBYTE_MACHINE_RUNTIME = if ([bool]$Cfg.MachineRuntimeEnabled) { '1' } else { '0' }
192
+ if ($cfg.ContainsKey('CodingRuntimeEnabled')) {
193
+ if ($cfg.CodingRuntimeEnabled -isnot [bool]) { Fail 'CodingRuntimeEnabled must be boolean' }
194
+ } else {
195
+ $cfg['CodingRuntimeEnabled'] = $false
196
+ }
197
+ if ($cfg.ContainsKey('CodingPolicyFile') -and -not [string]::IsNullOrWhiteSpace([string]$cfg.CodingPolicyFile)) {
198
+ $codingPolicy = [System.IO.Path]::GetFullPath([string]$cfg.CodingPolicyFile)
199
+ if (-not (Test-Path -LiteralPath $codingPolicy -PathType Leaf)) { Fail "CodingPolicyFile missing: $codingPolicy" }
200
+ $cfg['CodingPolicyFile'] = $codingPolicy
201
+ } else {
202
+ $cfg['CodingPolicyFile'] = ''
203
+ }
204
+ if ([bool]$cfg.CodingRuntimeEnabled -and [string]::IsNullOrWhiteSpace([string]$cfg.CodingPolicyFile)) { Fail 'CodingRuntimeEnabled requires CodingPolicyFile' }
205
+ $semanticConfigFile = if ($cfg.ContainsKey('SemanticLspConfigFile')) { [string]$cfg.SemanticLspConfigFile } else { '' }
206
+ $semanticConfigSha = if ($cfg.ContainsKey('SemanticLspConfigSha256')) { ([string]$cfg.SemanticLspConfigSha256).ToLowerInvariant() } else { '' }
207
+ if ([string]::IsNullOrWhiteSpace($semanticConfigFile) -ne [string]::IsNullOrWhiteSpace($semanticConfigSha)) { Fail 'SemanticLspConfigFile and SemanticLspConfigSha256 must be supplied together' }
208
+ if (-not [string]::IsNullOrWhiteSpace($semanticConfigFile)) {
209
+ if (-not [bool]$cfg.CodingRuntimeEnabled) { Fail 'SemanticLspConfigFile requires CodingRuntimeEnabled' }
210
+ if ($semanticConfigSha -cnotmatch '^[0-9a-f]{64}$') { Fail 'SemanticLspConfigSha256 invalid' }
211
+ $semanticConfigFile = [System.IO.Path]::GetFullPath($semanticConfigFile)
212
+ if (-not (Test-Path -LiteralPath $semanticConfigFile -PathType Leaf)) { Fail "SemanticLspConfigFile missing: $semanticConfigFile" }
213
+ $semanticItem = Get-Item -LiteralPath $semanticConfigFile -Force
214
+ if (($semanticItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { Fail 'SemanticLspConfigFile must not be a reparse point' }
215
+ $semanticObserved = Get-ExecutableHash $semanticConfigFile
216
+ if ($semanticObserved -ne $semanticConfigSha) { Fail "semantic LSP config SHA-256 mismatch: expected $semanticConfigSha, found $semanticObserved" }
217
+ $cfg['SemanticLspConfigFile'] = $semanticConfigFile
218
+ $cfg['SemanticLspConfigSha256'] = $semanticConfigSha
219
+ } else {
220
+ $cfg['SemanticLspConfigFile'] = ''
221
+ $cfg['SemanticLspConfigSha256'] = ''
222
+ }
223
+ if ($cfg.ContainsKey('MachineFsRuntimeEnabled')) {
224
+ if ($cfg.MachineFsRuntimeEnabled -isnot [bool]) { Fail 'MachineFsRuntimeEnabled must be boolean' }
225
+ } else {
226
+ $cfg['MachineFsRuntimeEnabled'] = $false
227
+ }
228
+ if ([bool]$cfg.MachineFsRuntimeEnabled -and -not [bool]$cfg.CodingRuntimeEnabled) { Fail 'MachineFsRuntimeEnabled requires CodingRuntimeEnabled' }
229
+ if ($cfg.ContainsKey('ObservationRuntimeEnabled')) {
230
+ if ($cfg.ObservationRuntimeEnabled -isnot [bool]) { Fail 'ObservationRuntimeEnabled must be boolean' }
231
+ } else {
232
+ $cfg['ObservationRuntimeEnabled'] = $false
233
+ }
234
+ if ([bool]$cfg.ObservationRuntimeEnabled -and -not [bool]$cfg.MachineRuntimeEnabled) { Fail 'ObservationRuntimeEnabled requires MachineRuntimeEnabled' }
235
+ if ([bool]$cfg.ObservationRuntimeEnabled -and -not [bool]$cfg.CodingRuntimeEnabled) { Fail 'ObservationRuntimeEnabled requires CodingRuntimeEnabled' }
236
+ if ($cfg.ContainsKey('DesktopRuntimeEnabled')) {
237
+ if ($cfg.DesktopRuntimeEnabled -isnot [bool]) { Fail 'DesktopRuntimeEnabled must be boolean' }
238
+ } else {
239
+ $cfg['DesktopRuntimeEnabled'] = $false
240
+ }
241
+ if ($cfg.ContainsKey('DesktopPolicyFile') -and -not [string]::IsNullOrWhiteSpace([string]$cfg.DesktopPolicyFile)) {
242
+ $desktopPolicy = [System.IO.Path]::GetFullPath([string]$cfg.DesktopPolicyFile)
243
+ if (-not (Test-Path -LiteralPath $desktopPolicy -PathType Leaf)) { Fail "DesktopPolicyFile missing: $desktopPolicy" }
244
+ $cfg['DesktopPolicyFile'] = $desktopPolicy
245
+ } else {
246
+ $cfg['DesktopPolicyFile'] = ''
247
+ }
248
+ if ([bool]$cfg.DesktopRuntimeEnabled -and [string]::IsNullOrWhiteSpace([string]$cfg.DesktopPolicyFile)) { Fail 'DesktopRuntimeEnabled requires DesktopPolicyFile' }
249
+ if ($cfg.ContainsKey('AutonomousRuntimeEnabled')) {
250
+ if ($cfg.AutonomousRuntimeEnabled -isnot [bool]) { Fail 'AutonomousRuntimeEnabled must be boolean' }
251
+ } else {
252
+ $cfg['AutonomousRuntimeEnabled'] = $false
253
+ }
254
+ if ($cfg.ContainsKey('AutonomousPolicyFile') -and -not [string]::IsNullOrWhiteSpace([string]$cfg.AutonomousPolicyFile)) {
255
+ $autonomousPolicy = [System.IO.Path]::GetFullPath([string]$cfg.AutonomousPolicyFile)
256
+ if (-not (Test-Path -LiteralPath $autonomousPolicy -PathType Leaf)) { Fail "AutonomousPolicyFile missing: $autonomousPolicy" }
257
+ $cfg['AutonomousPolicyFile'] = $autonomousPolicy
258
+ } else {
259
+ $cfg['AutonomousPolicyFile'] = ''
260
+ }
261
+ if ([bool]$cfg.AutonomousRuntimeEnabled -and -not [bool]$cfg.CodingRuntimeEnabled) { Fail 'AutonomousRuntimeEnabled requires CodingRuntimeEnabled' }
262
+ if ([bool]$cfg.AutonomousRuntimeEnabled -and [string]::IsNullOrWhiteSpace([string]$cfg.AutonomousPolicyFile)) { Fail 'AutonomousRuntimeEnabled requires AutonomousPolicyFile' }
263
+ if ([bool]$cfg.AutonomousSupervisorEnabled -and -not [bool]$cfg.AutonomousRuntimeEnabled) { Fail 'AutonomousSupervisorEnabled requires AutonomousRuntimeEnabled' }
264
+ if ([int]$cfg.StartTimeoutSeconds -lt 1 -or [int]$cfg.StartTimeoutSeconds -gt 120) { Fail 'StartTimeoutSeconds out of range' }
265
+ if ([int]$cfg.ProbeTimeoutSeconds -lt 1 -or [int]$cfg.ProbeTimeoutSeconds -gt 15) { Fail 'ProbeTimeoutSeconds out of range' }
266
+ return $cfg
267
+ }
268
+
269
+ function Assert-ControllerRootBound($Cfg) {
270
+ $expected = [System.IO.Path]::GetFullPath((Join-Path ([string]$Cfg.CoreRoot) 'controller')).TrimEnd('\')
271
+ $actual = [System.IO.Path]::GetFullPath($Root).TrimEnd('\')
272
+ if (-not [string]::Equals($actual, $expected, [System.StringComparison]::OrdinalIgnoreCase)) {
273
+ Fail "controller root mismatch: running '$actual', expected '$expected'; use canonical DEADBYTE-MCP.cmd"
274
+ }
275
+ }
276
+
277
+ function Get-ExecutableHash([string]$Path) {
278
+ (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
279
+ }
280
+
281
+ function Normalize-ComponentDependencies([string]$Name, $Spec) {
282
+ if (-not $Spec.ContainsKey('Dependencies')) { $Spec['Dependencies'] = @(); return }
283
+ $dependencies = @($Spec.Dependencies)
284
+ if ($dependencies.Count -gt 16) { Fail "$Name dependency count exceeds 16" }
285
+ $normalized = @()
286
+ foreach ($dependency in $dependencies) {
287
+ $depPath = [System.IO.Path]::GetFullPath([string]$dependency.Path)
288
+ $depHash = ([string]$dependency.Sha256).ToLowerInvariant()
289
+ if ([string]::IsNullOrWhiteSpace($depPath) -or -not (Test-Path -LiteralPath $depPath -PathType Leaf)) { Fail "$Name dependency missing: $depPath" }
290
+ if ($depHash -cnotmatch '^[0-9a-f]{64}$') { Fail "$Name dependency SHA-256 invalid: $depPath" }
291
+ $item = Get-Item -LiteralPath $depPath -Force
292
+ if (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { Fail "$Name dependency must not be a reparse point: $depPath" }
293
+ $actual = Get-ExecutableHash $depPath
294
+ if ($actual -ne $depHash) { Fail "$Name dependency hash mismatch in trusted config: $depPath" }
295
+ $depName = if ([string]::IsNullOrWhiteSpace([string]$dependency.Name)) { [System.IO.Path]::GetFileName($depPath) } else { [string]$dependency.Name }
296
+ $normalized += [ordered]@{ Name=$depName; Path=$depPath; Sha256=$depHash }
297
+ }
298
+ $Spec['Dependencies'] = $normalized
299
+ }
300
+
301
+ function Assert-ComponentDependencies([string]$Name, $Spec, [string]$Phase) {
302
+ foreach ($dependency in @($Spec.Dependencies)) {
303
+ $depPath = [string]$dependency.Path
304
+ $expected = [string]$dependency.Sha256
305
+ if (-not (Test-Path -LiteralPath $depPath -PathType Leaf)) { Fail "$Name dependency missing ${Phase}: $depPath" }
306
+ $actual = Get-ExecutableHash $depPath
307
+ if ($actual -ne $expected) { Fail "$Name dependency hash mismatch ${Phase}: $depPath expected $expected found $actual" }
308
+ }
309
+ }
310
+
311
+ function Get-LimitedProcessImagePath([int]$ProcessId) {
312
+ if (-not ('DeadbyteProcessQuery' -as [type])) {
313
+ $source = @'
314
+ using System;
315
+ using System.Text;
316
+ using System.Runtime.InteropServices;
317
+ public static class DeadbyteProcessQuery {
318
+ [DllImport("kernel32.dll", SetLastError=true)] static extern IntPtr OpenProcess(uint access, bool inheritHandle, int processId);
319
+ [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] static extern bool QueryFullProcessImageName(IntPtr process, uint flags, StringBuilder path, ref int size);
320
+ [DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
321
+ public static string GetImagePath(int processId) { const uint PROCESS_QUERY_LIMITED_INFORMATION=0x1000; IntPtr h=OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,false,processId); if(h==IntPtr.Zero)return null; try { var b=new StringBuilder(32768); int n=b.Capacity; return QueryFullProcessImageName(h,0,b,ref n)?b.ToString():null; } finally { CloseHandle(h); } }
322
+ }
323
+ '@
324
+ Add-Type -TypeDefinition $source -Language CSharp -ErrorAction Stop
325
+ }
326
+ try { $path=[DeadbyteProcessQuery]::GetImagePath($ProcessId); if ($null -eq $path) { return '' }; return [string]$path } catch { return '' }
327
+ }
328
+
329
+ function Get-ProcessObservation([int]$ProcessId) {
330
+ try { $p = Get-Process -Id $ProcessId -ErrorAction Stop } catch { return $null }
331
+ $image = ''
332
+ try {
333
+ $cim = Get-CimInstance Win32_Process -Filter ("ProcessId={0}" -f $ProcessId) -ErrorAction Stop
334
+ if ($cim -and -not [string]::IsNullOrWhiteSpace([string]$cim.ExecutablePath)) {
335
+ $image = [string]$cim.ExecutablePath
336
+ }
337
+ } catch {}
338
+ if ([string]::IsNullOrWhiteSpace($image)) {
339
+ try { $image = [string]$p.Path } catch {}
340
+ }
341
+ if ([string]::IsNullOrWhiteSpace($image)) {
342
+ try { $image = Get-LimitedProcessImagePath $ProcessId } catch {}
343
+ }
344
+ if ([string]::IsNullOrWhiteSpace($image)) {
345
+ try { $image = [string]$p.MainModule.FileName } catch {}
346
+ }
347
+ if ([string]::IsNullOrWhiteSpace($image)) { return $null }
348
+ try { $image = [System.IO.Path]::GetFullPath($image) } catch { return $null }
349
+ if (-not (Test-Path -LiteralPath $image -PathType Leaf)) { return $null }
350
+ try { $imageHash = Get-ExecutableHash $image } catch { return $null }
351
+ [pscustomobject]@{
352
+ pid = [int]$p.Id
353
+ creation_time_utc = $p.StartTime.ToUniversalTime().ToString('o')
354
+ image_path = $image
355
+ image_sha256 = $imageHash
356
+ }
357
+ }
358
+
359
+ function Read-Ledger([string]$Name) {
360
+ $path = Ledger-Path $Name
361
+ if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { return $null }
362
+ try { return (Get-Content -LiteralPath $path -Raw -Encoding UTF8 | ConvertFrom-Json) } catch { return [pscustomobject]@{ corrupt = $true; path = $path } }
363
+ }
364
+
365
+ function Test-OwnedInstance([string]$Name) {
366
+ $ledger = Read-Ledger $Name
367
+ if (-not $ledger) { return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='no-ledger'; Ledger=$null; Observation=$null } }
368
+ if ($ledger.PSObject.Properties.Name -contains 'corrupt' -and $ledger.corrupt -eq $true) { return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='corrupt-ledger'; Ledger=$ledger; Observation=$null } }
369
+ if ($ledger.schema -ne $ControllerSchema -or -not $ledger.instance_nonce) { return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='ledger-schema'; Ledger=$ledger; Observation=$null } }
370
+ if ($Name -in @('Bridge','Supervisor')) {
371
+ if (-not ($ledger.PSObject.Properties.Name -contains 'script_path') -or -not ($ledger.PSObject.Properties.Name -contains 'script_sha256')) {
372
+ return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='ledger-schema'; Ledger=$ledger; Observation=$null }
373
+ }
374
+ $scriptPath = [string]$ledger.script_path
375
+ $scriptHash = [string]$ledger.script_sha256
376
+ if ([string]::IsNullOrWhiteSpace($scriptPath) -or $scriptHash -cnotmatch '^[0-9a-f]{64}$' -or -not (Test-Path -LiteralPath $scriptPath -PathType Leaf)) {
377
+ return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='script-hash-mismatch'; Ledger=$ledger; Observation=$null }
378
+ }
379
+ try { $observedScriptHash = Get-ExecutableHash $scriptPath } catch { $observedScriptHash = '' }
380
+ if ($observedScriptHash -ne $scriptHash) { return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='script-hash-mismatch'; Ledger=$ledger; Observation=$null } }
381
+ }
382
+ if ($ledger.PSObject.Properties.Name -contains 'dependencies') {
383
+ foreach ($dependency in @($ledger.dependencies)) {
384
+ $depPath = [string]$dependency.path
385
+ $depHash = [string]$dependency.sha256
386
+ if ([string]::IsNullOrWhiteSpace($depPath) -or $depHash -cnotmatch '^[0-9a-f]{64}$' -or -not (Test-Path -LiteralPath $depPath -PathType Leaf)) {
387
+ return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='dependency-hash-mismatch'; Ledger=$ledger; Observation=$null }
388
+ }
389
+ try { $observedDependencyHash = Get-ExecutableHash $depPath } catch { $observedDependencyHash = '' }
390
+ if ($observedDependencyHash -ne $depHash) { return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='dependency-hash-mismatch'; Ledger=$ledger; Observation=$null } }
391
+ }
392
+ }
393
+ $obs = Get-ProcessObservation ([int]$ledger.pid)
394
+ if (-not $obs) { try { [void](Get-Process -Id ([int]$ledger.pid) -ErrorAction Stop); return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='process-unobservable'; Ledger=$ledger; Observation=$null } } catch { return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='process-missing'; Ledger=$ledger; Observation=$null } } }
395
+ if ($obs.creation_time_utc -ne [string]$ledger.creation_time_utc) { return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='pid-reused'; Ledger=$ledger; Observation=$obs } }
396
+ if (-not [string]::Equals($obs.image_path, [string]$ledger.image_path, [System.StringComparison]::OrdinalIgnoreCase)) { return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='image-path-mismatch'; Ledger=$ledger; Observation=$obs } }
397
+ if ($obs.image_sha256 -ne [string]$ledger.image_sha256) { return [pscustomobject]@{ Name=$Name; Alive=$false; Reason='image-hash-mismatch'; Ledger=$ledger; Observation=$obs } }
398
+ [pscustomobject]@{ Name=$Name; Alive=$true; Reason='verified'; Ledger=$ledger; Observation=$obs }
399
+ }
400
+
401
+ function Reconcile-PidReusedLedger([string]$Name, $Ownership) {
402
+ if ($null -eq $Ownership -or [string]$Ownership.Reason -ne 'pid-reused') { Fail "$Name PID-reuse reconcile requires pid-reused ownership state" }
403
+ if ($null -eq $Ownership.Ledger -or $null -eq $Ownership.Observation) { Fail "$Name PID-reuse reconcile requires stale ledger and current process observation" }
404
+ $ledger = $Ownership.Ledger
405
+ $observed = $Ownership.Observation
406
+ if ([int]$ledger.pid -ne [int]$observed.pid) { Fail "$Name PID-reuse reconcile PID mismatch" }
407
+ if ([string]$ledger.creation_time_utc -eq [string]$observed.creation_time_utc) { Fail "$Name PID-reuse reconcile requires creation-time mismatch" }
408
+ $ledgerPath = Ledger-Path $Name
409
+ if (-not (Test-Path -LiteralPath $ledgerPath -PathType Leaf)) { Fail "$Name PID-reuse stale ledger disappeared before archive" }
410
+ $ledgerSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $ledgerPath).Hash.ToLowerInvariant()
411
+ if ($ledgerSha256 -cnotmatch '^[0-9a-f]{64}$') { Fail "$Name PID-reuse stale ledger SHA-256 invalid" }
412
+ $evidenceDir = Join-Path $RuntimeDir 'ownership-evidence'
413
+ Ensure-Directory $evidenceDir
414
+ $archivePath = Join-Path $evidenceDir ("{0}.stale.{1}.json" -f $Name.ToLowerInvariant(), $ledgerSha256)
415
+ if (Test-Path -LiteralPath $archivePath -PathType Leaf) {
416
+ $existingSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $archivePath).Hash.ToLowerInvariant()
417
+ if ($existingSha256 -ne $ledgerSha256) { Fail "$Name PID-reuse evidence archive hash conflict" }
418
+ } else {
419
+ Copy-Item -LiteralPath $ledgerPath -Destination $archivePath
420
+ }
421
+ $archiveSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $archivePath).Hash.ToLowerInvariant()
422
+ if ($archiveSha256 -ne $ledgerSha256) { Fail "$Name PID-reuse evidence archive verification failed" }
423
+ $receipt = [ordered]@{
424
+ schema = 'deadbyte.ownership-recovery.v1'
425
+ component = $Name.ToLowerInvariant()
426
+ reason = 'pid-reused'
427
+ stale_ledger_sha256 = $ledgerSha256
428
+ stale_ledger_archive = $archivePath
429
+ stale_pid = [int]$ledger.pid
430
+ stale_creation_time_utc = [string]$ledger.creation_time_utc
431
+ stale_image_path = [string]$ledger.image_path
432
+ stale_image_sha256 = [string]$ledger.image_sha256
433
+ observed_pid = [int]$observed.pid
434
+ observed_creation_time_utc = [string]$observed.creation_time_utc
435
+ observed_image_path = [string]$observed.image_path
436
+ observed_image_sha256 = [string]$observed.image_sha256
437
+ reconciled_at_utc = [DateTime]::UtcNow.ToString('o')
438
+ action = 'archive-stale-ledger-remove-metadata-no-process-kill'
439
+ }
440
+ $receiptPath = Join-Path $evidenceDir ("{0}.recovery.{1}.json" -f $Name.ToLowerInvariant(), $ledgerSha256)
441
+ $receiptTemp = $receiptPath + '.tmp'
442
+ $receiptJson = $receipt | ConvertTo-Json -Depth 5
443
+ [System.IO.File]::WriteAllText($receiptTemp, $receiptJson, (New-Object System.Text.UTF8Encoding($false)))
444
+ Move-Item -LiteralPath $receiptTemp -Destination $receiptPath -Force
445
+ Remove-Item -LiteralPath (Ledger-Path $Name) -Force
446
+ if (Test-Path -LiteralPath (Ledger-Path $Name) -PathType Leaf) { Fail "$Name PID-reuse stale ownership metadata removal failed" }
447
+ return [pscustomobject]@{ Name=$Name; Reason='pid-reused-reconciled'; LedgerSha256=$ledgerSha256; ArchivePath=$archivePath; ReceiptPath=$receiptPath }
448
+ }
449
+
450
+ function Write-Ledger([string]$Name, $Observation, [string]$Nonce, $Spec = $null) {
451
+ Ensure-Directory $RuntimeDir
452
+ $body = [ordered]@{
453
+ schema = $ControllerSchema
454
+ component = $Name.ToLowerInvariant()
455
+ instance_nonce = $Nonce
456
+ pid = [int]$Observation.pid
457
+ creation_time_utc = [string]$Observation.creation_time_utc
458
+ image_path = [string]$Observation.image_path
459
+ image_sha256 = [string]$Observation.image_sha256
460
+ }
461
+ if ($Name -in @('Bridge','Supervisor')) {
462
+ if (-not $Spec) { Fail "$Name ledger requires launch specification" }
463
+ $body['script_path'] = [string]$Spec.ScriptPath
464
+ $body['script_sha256'] = [string]$Spec.ScriptSha256
465
+ }
466
+ if ($Spec -and @($Spec.Dependencies).Count -gt 0) {
467
+ $body['dependencies'] = @($Spec.Dependencies | ForEach-Object { [ordered]@{ name=[string]$_.Name; path=[string]$_.Path; sha256=[string]$_.Sha256 } })
468
+ }
469
+ $tmp = (Ledger-Path $Name) + '.tmp'
470
+ $body | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $tmp -Encoding UTF8
471
+ Move-Item -LiteralPath $tmp -Destination (Ledger-Path $Name) -Force
472
+ }
473
+
474
+ function Start-OwnedComponent([string]$Name, $Spec, $Cfg = $null) {
475
+ $current = Test-OwnedInstance $Name
476
+ if ($current.Alive) { return $current }
477
+ if ($current.Reason -eq 'pid-reused') {
478
+ Reconcile-PidReusedLedger $Name $current | Out-Null
479
+ $current = Test-OwnedInstance $Name
480
+ if ($current.Reason -ne 'no-ledger') { Fail "$Name PID-reuse reconcile did not clear stale ownership metadata" }
481
+ }
482
+ if ($current.Reason -in @('process-unobservable','image-path-mismatch','image-hash-mismatch','script-hash-mismatch','dependency-hash-mismatch','corrupt-ledger','ledger-schema')) {
483
+ Fail "$Name ownership conflict: $($current.Reason); refusing to attach or kill an unverified process"
484
+ }
485
+ Remove-Item -LiteralPath (Ledger-Path $Name) -Force -ErrorAction SilentlyContinue
486
+ if ($Name -eq 'Cloud') { Remove-Item -LiteralPath (Join-Path $RuntimeDir 'current-origin.txt') -Force -ErrorAction SilentlyContinue }
487
+ Ensure-Directory $LogDir
488
+ $stdout = Log-Path $Name 'stdout'
489
+ $stderr = Log-Path $Name 'stderr'
490
+ Remove-Item -LiteralPath $stdout,$stderr -Force -ErrorAction SilentlyContinue
491
+ $nonceBytes = New-Object byte[] 32
492
+ [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($nonceBytes)
493
+ $nonce = -join ($nonceBytes | ForEach-Object { $_.ToString('x2') })
494
+ $componentExecutableHash = Get-ExecutableHash ([string]$Spec.FilePath)
495
+ if ($componentExecutableHash -ne [string]$Spec.FileSha256) { Fail "$Name executable hash mismatch before start: expected $($Spec.FileSha256), found $componentExecutableHash" }
496
+ Assert-ComponentDependencies $Name $Spec 'before start'
497
+ if ($Name -in @('Bridge','Supervisor')) {
498
+ $scriptHash = Get-ExecutableHash ([string]$Spec.ScriptPath)
499
+ if ($scriptHash -ne [string]$Spec.ScriptSha256) { Fail "$Name script hash mismatch before start: expected $($Spec.ScriptSha256), found $scriptHash" }
500
+ }
501
+ $args = [string]$Spec.Arguments
502
+ $start = @{ FilePath=[string]$Spec.FilePath; WorkingDirectory=[string]$Spec.WorkingDirectory; PassThru=$true; WindowStyle='Hidden'; RedirectStandardOutput=$stdout; RedirectStandardError=$stderr }
503
+ if (-not [string]::IsNullOrWhiteSpace($args)) { $start.ArgumentList = $args }
504
+ $hadCoreRootEnv = Test-Path Env:DEADBYTE_CORE_ROOT
505
+ $previousCoreRootEnv = $env:DEADBYTE_CORE_ROOT
506
+ $hadHostPolicyEnv = Test-Path Env:DEADBYTE_PRIVILEGED_POLICY
507
+ $previousHostPolicyEnv = $env:DEADBYTE_PRIVILEGED_POLICY
508
+ $hadWorkspaceRootEnv = Test-Path Env:DEADBYTE_WORKSPACE_ROOT
509
+ $previousWorkspaceRootEnv = $env:DEADBYTE_WORKSPACE_ROOT
510
+ $hadAuditRuntimeEnv = Test-Path Env:DEADBYTE_AUDIT_RUNTIME
511
+ $previousAuditRuntimeEnv = $env:DEADBYTE_AUDIT_RUNTIME
512
+ $hadMachineRuntimeEnv = Test-Path Env:DEADBYTE_MACHINE_RUNTIME
513
+ $previousMachineRuntimeEnv = $env:DEADBYTE_MACHINE_RUNTIME
514
+ $hadAgentRuntimeEnv = Test-Path Env:DEADBYTE_AGENT_RUNTIME
515
+ $previousAgentRuntimeEnv = $env:DEADBYTE_AGENT_RUNTIME
516
+ $hadProcessRuntimeEnv = Test-Path Env:DEADBYTE_PROCESS_RUNTIME
517
+ $previousProcessRuntimeEnv = $env:DEADBYTE_PROCESS_RUNTIME
518
+ $hadProcessPolicyEnv = Test-Path Env:DEADBYTE_PROCESS_POLICY
519
+ $previousProcessPolicyEnv = $env:DEADBYTE_PROCESS_POLICY
520
+ $hadCodingRuntimeEnv = Test-Path Env:DEADBYTE_CODING_RUNTIME
521
+ $previousCodingRuntimeEnv = $env:DEADBYTE_CODING_RUNTIME
522
+ $hadMachineFsRuntimeEnv = Test-Path Env:DEADBYTE_MACHINE_FS_RUNTIME
523
+ $previousMachineFsRuntimeEnv = $env:DEADBYTE_MACHINE_FS_RUNTIME
524
+ $hadObservationRuntimeEnv = Test-Path Env:DEADBYTE_OBSERVATION_RUNTIME
525
+ $previousObservationRuntimeEnv = $env:DEADBYTE_OBSERVATION_RUNTIME
526
+ $hadDesktopRuntimeEnv = Test-Path Env:DEADBYTE_DESKTOP_RUNTIME
527
+ $previousDesktopRuntimeEnv = $env:DEADBYTE_DESKTOP_RUNTIME
528
+ $hadDesktopPolicyEnv = Test-Path Env:DEADBYTE_DESKTOP_POLICY
529
+ $previousDesktopPolicyEnv = $env:DEADBYTE_DESKTOP_POLICY
530
+ $hadCodingPolicyEnv = Test-Path Env:DEADBYTE_CODING_POLICY
531
+ $previousCodingPolicyEnv = $env:DEADBYTE_CODING_POLICY
532
+ $hadSemanticConfigEnv = Test-Path Env:DEADBYTE_SEMANTIC_LSP_CONFIG
533
+ $previousSemanticLspConfigEnv = $env:DEADBYTE_SEMANTIC_LSP_CONFIG
534
+ $hadSemanticConfigShaEnv = Test-Path Env:DEADBYTE_SEMANTIC_LSP_CONFIG_SHA256
535
+ $previousSemanticLspConfigShaEnv = $env:DEADBYTE_SEMANTIC_LSP_CONFIG_SHA256
536
+ $hadAutonomousRuntimeEnv = Test-Path Env:DEADBYTE_AUTONOMOUS_RUNTIME
537
+ $previousAutonomousRuntimeEnv = $env:DEADBYTE_AUTONOMOUS_RUNTIME
538
+ $hadAutonomousPolicyEnv = Test-Path Env:DEADBYTE_AUTONOMOUS_POLICY
539
+ $previousAutonomousPolicyEnv = $env:DEADBYTE_AUTONOMOUS_POLICY
540
+ $hadSupervisorOnceEnv = Test-Path Env:DEADBYTE_SUPERVISOR_ONCE
541
+ $previousSupervisorOnceEnv = $env:DEADBYTE_SUPERVISOR_ONCE
542
+ $hadSupervisorMarkerEnv = Test-Path Env:DEADBYTE_SUPERVISOR_MARKER
543
+ $previousSupervisorMarkerEnv = $env:DEADBYTE_SUPERVISOR_MARKER
544
+ $hadAutonomousSupervisorModeEnv = Test-Path Env:DEADBYTE_AUTONOMOUS_SUPERVISOR_MODE
545
+ $previousAutonomousSupervisorModeEnv = $env:DEADBYTE_AUTONOMOUS_SUPERVISOR_MODE
546
+ $hadCapabilityProfileEnv = Test-Path Env:DEADBYTE_CAPABILITY_PROFILE
547
+ $previousCapabilityProfileEnv = $env:DEADBYTE_CAPABILITY_PROFILE
548
+ try {
549
+ if ($Name -eq 'Bridge' -and $Cfg) {
550
+ $env:DEADBYTE_CAPABILITY_PROFILE = 'compact'
551
+ $env:DEADBYTE_AUDIT_RUNTIME = '1'
552
+ $env:DEADBYTE_CORE_ROOT = [string]$Cfg.CoreRoot
553
+ $env:DEADBYTE_WORKSPACE_ROOT = [string]$Cfg.WorkspaceRoot
554
+ $env:DEADBYTE_MACHINE_RUNTIME = if ([bool]$Cfg.MachineRuntimeEnabled) { '1' } else { '0' }
555
555
  $env:DEADBYTE_AGENT_RUNTIME = if ([bool]$Cfg.AgentRuntimeEnabled) { '1' } else { '0' }
556
- $env:DEADBYTE_PROCESS_RUNTIME = if ([bool]$Cfg.ProcessRuntimeEnabled) { '1' } else { '0' }
557
- if (-not [string]::IsNullOrWhiteSpace([string]$Cfg.ProcessPolicyFile)) {
558
- $env:DEADBYTE_PROCESS_POLICY = [string]$Cfg.ProcessPolicyFile
559
- }
560
- $env:DEADBYTE_CODING_RUNTIME = if ([bool]$Cfg.CodingRuntimeEnabled) { '1' } else { '0' }
561
- $env:DEADBYTE_MACHINE_FS_RUNTIME = if ([bool]$Cfg.MachineFsRuntimeEnabled) { '1' } else { '0' }
562
- $env:DEADBYTE_OBSERVATION_RUNTIME = if ([bool]$Cfg.ObservationRuntimeEnabled) { '1' } else { '0' }
563
- $env:DEADBYTE_DESKTOP_RUNTIME = if ([bool]$Cfg.DesktopRuntimeEnabled) { '1' } else { '0' }
564
- if (-not [string]::IsNullOrWhiteSpace([string]$Cfg.DesktopPolicyFile)) {
565
- $env:DEADBYTE_DESKTOP_POLICY = [string]$Cfg.DesktopPolicyFile
566
- } else {
567
- Remove-Item Env:DEADBYTE_DESKTOP_POLICY -ErrorAction SilentlyContinue
568
- }
569
- if (-not [string]::IsNullOrWhiteSpace([string]$Cfg.CodingPolicyFile)) {
570
- $env:DEADBYTE_CODING_POLICY = [string]$Cfg.CodingPolicyFile
571
- }
572
- if (-not [string]::IsNullOrWhiteSpace([string]$Cfg.SemanticLspConfigFile)) {
573
- $env:DEADBYTE_SEMANTIC_LSP_CONFIG = [string]$Cfg.SemanticLspConfigFile
574
- $env:DEADBYTE_SEMANTIC_LSP_CONFIG_SHA256 = [string]$Cfg.SemanticLspConfigSha256
575
- } else {
576
- Remove-Item Env:DEADBYTE_SEMANTIC_LSP_CONFIG -ErrorAction SilentlyContinue
577
- Remove-Item Env:DEADBYTE_SEMANTIC_LSP_CONFIG_SHA256 -ErrorAction SilentlyContinue
578
- }
579
- $env:DEADBYTE_AUTONOMOUS_RUNTIME = if ([bool]$Cfg.AutonomousRuntimeEnabled) { '1' } else { '0' }
580
- $env:DEADBYTE_AUTONOMOUS_SUPERVISOR_MODE = if ([bool]$Cfg.AutonomousSupervisorEnabled) { 'external' } else { 'in_process' }
581
- if (-not [string]::IsNullOrWhiteSpace([string]$Cfg.AutonomousPolicyFile)) {
582
- $env:DEADBYTE_AUTONOMOUS_POLICY = [string]$Cfg.AutonomousPolicyFile
583
- }
584
- if (-not [string]::IsNullOrWhiteSpace([string]$Cfg.HostPolicyFile)) {
585
- $env:DEADBYTE_PRIVILEGED_POLICY = [string]$Cfg.HostPolicyFile
586
- }
587
- } elseif ($Name -eq 'Supervisor' -and $Cfg) {
588
- Remove-Item Env:DEADBYTE_CAPABILITY_PROFILE -ErrorAction SilentlyContinue
589
- Remove-Item Env:DEADBYTE_AUDIT_RUNTIME -ErrorAction SilentlyContinue
590
- Remove-Item Env:DEADBYTE_PRIVILEGED_POLICY -ErrorAction SilentlyContinue
591
- Remove-Item Env:DEADBYTE_MACHINE_RUNTIME -ErrorAction SilentlyContinue
592
- Remove-Item Env:DEADBYTE_AGENT_RUNTIME -ErrorAction SilentlyContinue
593
- Remove-Item Env:DEADBYTE_PROCESS_RUNTIME -ErrorAction SilentlyContinue
594
- Remove-Item Env:DEADBYTE_PROCESS_POLICY -ErrorAction SilentlyContinue
595
- Remove-Item Env:DEADBYTE_MACHINE_FS_RUNTIME -ErrorAction SilentlyContinue
596
- Remove-Item Env:DEADBYTE_OBSERVATION_RUNTIME -ErrorAction SilentlyContinue
597
- Remove-Item Env:DEADBYTE_DESKTOP_RUNTIME -ErrorAction SilentlyContinue
598
- Remove-Item Env:DEADBYTE_DESKTOP_POLICY -ErrorAction SilentlyContinue
599
- Remove-Item Env:DEADBYTE_SEMANTIC_LSP_CONFIG -ErrorAction SilentlyContinue
600
- Remove-Item Env:DEADBYTE_SEMANTIC_LSP_CONFIG_SHA256 -ErrorAction SilentlyContinue
601
- Remove-Item Env:DEADBYTE_AUTONOMOUS_SUPERVISOR_MODE -ErrorAction SilentlyContinue
602
- $env:DEADBYTE_CODING_RUNTIME = '1'
603
- $env:DEADBYTE_AUTONOMOUS_RUNTIME = '1'
604
- $env:DEADBYTE_CODING_POLICY = [string]$Cfg.CodingPolicyFile
605
- $env:DEADBYTE_AUTONOMOUS_POLICY = [string]$Cfg.AutonomousPolicyFile
606
- $env:DEADBYTE_SUPERVISOR_ONCE = '0'
607
- $env:DEADBYTE_SUPERVISOR_MARKER = Join-Path $RuntimeDir 'autonomous-supervisor-owner.json'
608
- } else {
609
- Remove-Item Env:DEADBYTE_CAPABILITY_PROFILE -ErrorAction SilentlyContinue
610
- Remove-Item Env:DEADBYTE_AUDIT_RUNTIME -ErrorAction SilentlyContinue
611
- }
612
- $proc = Start-Process @start
613
- } finally {
614
- if ($hadCoreRootEnv) { $env:DEADBYTE_CORE_ROOT = $previousCoreRootEnv }
615
- else { Remove-Item Env:DEADBYTE_CORE_ROOT -ErrorAction SilentlyContinue }
616
- if ($hadHostPolicyEnv) { $env:DEADBYTE_PRIVILEGED_POLICY = $previousHostPolicyEnv }
617
- else { Remove-Item Env:DEADBYTE_PRIVILEGED_POLICY -ErrorAction SilentlyContinue }
618
- if ($hadWorkspaceRootEnv) { $env:DEADBYTE_WORKSPACE_ROOT = $previousWorkspaceRootEnv }
619
- else { Remove-Item Env:DEADBYTE_WORKSPACE_ROOT -ErrorAction SilentlyContinue }
620
- if ($hadAuditRuntimeEnv) { $env:DEADBYTE_AUDIT_RUNTIME = $previousAuditRuntimeEnv }
621
- else { Remove-Item Env:DEADBYTE_AUDIT_RUNTIME -ErrorAction SilentlyContinue }
622
- if ($hadMachineRuntimeEnv) { $env:DEADBYTE_MACHINE_RUNTIME = $previousMachineRuntimeEnv }
623
- else { Remove-Item Env:DEADBYTE_MACHINE_RUNTIME -ErrorAction SilentlyContinue }
624
- if ($hadAgentRuntimeEnv) { $env:DEADBYTE_AGENT_RUNTIME = $previousAgentRuntimeEnv }
625
- else { Remove-Item Env:DEADBYTE_AGENT_RUNTIME -ErrorAction SilentlyContinue }
626
- if ($hadProcessRuntimeEnv) { $env:DEADBYTE_PROCESS_RUNTIME = $previousProcessRuntimeEnv }
627
- else { Remove-Item Env:DEADBYTE_PROCESS_RUNTIME -ErrorAction SilentlyContinue }
628
- if ($hadProcessPolicyEnv) { $env:DEADBYTE_PROCESS_POLICY = $previousProcessPolicyEnv }
629
- else { Remove-Item Env:DEADBYTE_PROCESS_POLICY -ErrorAction SilentlyContinue }
630
- if ($hadCodingRuntimeEnv) { $env:DEADBYTE_CODING_RUNTIME = $previousCodingRuntimeEnv }
631
- else { Remove-Item Env:DEADBYTE_CODING_RUNTIME -ErrorAction SilentlyContinue }
632
- if ($hadMachineFsRuntimeEnv) { $env:DEADBYTE_MACHINE_FS_RUNTIME = $previousMachineFsRuntimeEnv }
633
- else { Remove-Item Env:DEADBYTE_MACHINE_FS_RUNTIME -ErrorAction SilentlyContinue }
634
- if ($hadObservationRuntimeEnv) { $env:DEADBYTE_OBSERVATION_RUNTIME = $previousObservationRuntimeEnv }
635
- else { Remove-Item Env:DEADBYTE_OBSERVATION_RUNTIME -ErrorAction SilentlyContinue }
636
- if ($hadDesktopRuntimeEnv) { $env:DEADBYTE_DESKTOP_RUNTIME = $previousDesktopRuntimeEnv }
637
- else { Remove-Item Env:DEADBYTE_DESKTOP_RUNTIME -ErrorAction SilentlyContinue }
638
- if ($hadDesktopPolicyEnv) { $env:DEADBYTE_DESKTOP_POLICY = $previousDesktopPolicyEnv }
639
- else { Remove-Item Env:DEADBYTE_DESKTOP_POLICY -ErrorAction SilentlyContinue }
640
- if ($hadCodingPolicyEnv) { $env:DEADBYTE_CODING_POLICY = $previousCodingPolicyEnv }
641
- else { Remove-Item Env:DEADBYTE_CODING_POLICY -ErrorAction SilentlyContinue }
642
- if ($hadSemanticConfigEnv) { $env:DEADBYTE_SEMANTIC_LSP_CONFIG = $previousSemanticLspConfigEnv }
643
- else { Remove-Item Env:DEADBYTE_SEMANTIC_LSP_CONFIG -ErrorAction SilentlyContinue }
644
- if ($hadSemanticConfigShaEnv) { $env:DEADBYTE_SEMANTIC_LSP_CONFIG_SHA256 = $previousSemanticLspConfigShaEnv }
645
- else { Remove-Item Env:DEADBYTE_SEMANTIC_LSP_CONFIG_SHA256 -ErrorAction SilentlyContinue }
646
- if ($hadAutonomousRuntimeEnv) { $env:DEADBYTE_AUTONOMOUS_RUNTIME = $previousAutonomousRuntimeEnv }
647
- else { Remove-Item Env:DEADBYTE_AUTONOMOUS_RUNTIME -ErrorAction SilentlyContinue }
648
- if ($hadAutonomousPolicyEnv) { $env:DEADBYTE_AUTONOMOUS_POLICY = $previousAutonomousPolicyEnv }
649
- else { Remove-Item Env:DEADBYTE_AUTONOMOUS_POLICY -ErrorAction SilentlyContinue }
650
- if ($hadSupervisorOnceEnv) { $env:DEADBYTE_SUPERVISOR_ONCE = $previousSupervisorOnceEnv }
651
- else { Remove-Item Env:DEADBYTE_SUPERVISOR_ONCE -ErrorAction SilentlyContinue }
652
- if ($hadSupervisorMarkerEnv) { $env:DEADBYTE_SUPERVISOR_MARKER = $previousSupervisorMarkerEnv }
653
- else { Remove-Item Env:DEADBYTE_SUPERVISOR_MARKER -ErrorAction SilentlyContinue }
654
- if ($hadAutonomousSupervisorModeEnv) { $env:DEADBYTE_AUTONOMOUS_SUPERVISOR_MODE = $previousAutonomousSupervisorModeEnv }
655
- else { Remove-Item Env:DEADBYTE_AUTONOMOUS_SUPERVISOR_MODE -ErrorAction SilentlyContinue }
656
- if ($hadCapabilityProfileEnv) { $env:DEADBYTE_CAPABILITY_PROFILE = $previousCapabilityProfileEnv }
657
- else { Remove-Item Env:DEADBYTE_CAPABILITY_PROFILE -ErrorAction SilentlyContinue }
658
- }
659
- Start-Sleep -Milliseconds 150
660
- $obs = Get-ProcessObservation ([int]$proc.Id)
661
- if (-not $obs) { Fail "$Name exited before identity capture; see $stderr" }
662
- $expected = [System.IO.Path]::GetFullPath([string]$Spec.FilePath)
663
- if (-not [string]::Equals($obs.image_path, $expected, [System.StringComparison]::OrdinalIgnoreCase)) {
664
- try { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue } catch {}
665
- Fail "$Name spawned a wrapper instead of the configured long-lived executable; observed '$($obs.image_path)'"
666
- }
667
- if ($obs.image_sha256 -ne (Get-ExecutableHash $expected)) {
668
- try { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue } catch {}
669
- Fail "$Name executable changed during start"
670
- }
671
- Write-Ledger $Name $obs $nonce $Spec
672
- return Test-OwnedInstance $Name
673
- }
674
-
675
- function Stop-OwnedComponent([string]$Name) {
676
- $owned = Test-OwnedInstance $Name
677
- if (-not $owned.Ledger) { return }
678
- if (-not $owned.Alive) {
679
- if ($owned.Reason -eq 'process-missing') { Remove-Item -LiteralPath (Ledger-Path $Name) -Force -ErrorAction SilentlyContinue; return }
680
- if ($owned.Reason -eq 'pid-reused') { Reconcile-PidReusedLedger $Name $owned | Out-Null; return }
681
- Fail "$Name is not provably owned ($($owned.Reason)); refusing Stop-Process"
682
- }
683
- Stop-Process -Id ([int]$owned.Ledger.pid) -Force -ErrorAction Stop
684
- try { Wait-Process -Id ([int]$owned.Ledger.pid) -Timeout 5 -ErrorAction SilentlyContinue } catch {}
685
- Remove-Item -LiteralPath (Ledger-Path $Name) -Force -ErrorAction SilentlyContinue
686
- }
687
-
688
- function Get-LocalHealth($Cfg) {
689
- $response = $null
690
- try {
691
- $request = [System.Net.HttpWebRequest]::Create([string]$Cfg.LocalReadyUri)
692
- $request.Timeout = [int]$Cfg.ProbeTimeoutSeconds * 1000
693
- $request.ReadWriteTimeout = [int]$Cfg.ProbeTimeoutSeconds * 1000
694
- $response = $request.GetResponse()
695
- } catch [System.Net.WebException] {
696
- $response = $_.Exception.Response
697
- if ($null -eq $response) { return [pscustomobject]@{ State='UNREACHABLE'; StatusCode=0; Diagnostic='readiness endpoint unreachable' } }
698
- } catch { return [pscustomobject]@{ State='UNREACHABLE'; StatusCode=0; Diagnostic='readiness probe failed' } }
699
- try {
700
- $status = [int]$response.StatusCode
701
- $reader = New-Object System.IO.StreamReader($response.GetResponseStream())
702
- try { $body = $reader.ReadToEnd() } finally { $reader.Dispose() }
703
- try { $parsed = $body | ConvertFrom-Json } catch { return [pscustomobject]@{ State='INVALID'; StatusCode=$status; Diagnostic='readiness response JSON invalid' } }
704
- if ($parsed.schema -ne 'deadbyte.bridge-health.v1' -or [string]$parsed.state -notin @('STARTING','READY','DEGRADED','RECONCILING','STOPPING')) {
705
- return [pscustomobject]@{ State='INVALID'; StatusCode=$status; Diagnostic='readiness response schema invalid' }
706
- }
707
- return [pscustomobject]@{ State=[string]$parsed.state; StatusCode=$status; Diagnostic=[string]$parsed.diagnostic }
708
- } finally { if ($null -ne $response) { $response.Dispose() } }
709
- }
710
- function Test-LocalReady($Cfg) {
711
- $health = Get-LocalHealth $Cfg
712
- return ($health.State -eq 'READY' -and $health.StatusCode -ge 200 -and $health.StatusCode -lt 300)
713
- }
714
- function Get-LocalReadyListenerOwners($Cfg) {
715
- try { $uri = [Uri]([string]$Cfg.LocalReadyUri) } catch { Fail 'LocalReadyUri is invalid' }
716
- if (-not $uri.IsLoopback) { Fail 'LocalReadyUri must be loopback for owned readiness attribution' }
717
- try {
718
- $listeners = @(Get-NetTCPConnection -State Listen -ErrorAction Stop | Where-Object {
719
- [int]$_.LocalPort -eq [int]$uri.Port -and (
720
- [string]$_.LocalAddress -eq [string]$uri.Host -or
721
- [string]$_.LocalAddress -eq '0.0.0.0' -or
722
- [string]$_.LocalAddress -eq '::'
723
- )
724
- })
725
- } catch { Fail "cannot inspect listener ownership for local readiness endpoint: $($_.Exception.Message)" }
726
- return @($listeners | ForEach-Object { [int]$_.OwningProcess } | Sort-Object -Unique)
727
- }
728
-
729
- function Assert-LocalReadyPortAvailable($Cfg) {
730
- $owners = @(Get-LocalReadyListenerOwners $Cfg)
731
- if ($owners.Count -gt 0) {
732
- Fail ("local readiness endpoint already has listener pid(s)={0}; refusing to start an unverified Bridge" -f ($owners -join ','))
733
- }
734
- }
735
-
736
- function Test-LocalReadyOwned($Cfg, $Bridge) {
737
- if (-not $Bridge -or -not $Bridge.Alive) { return $false }
738
- $owners = @(Get-LocalReadyListenerOwners $Cfg)
739
- if ($owners.Count -ne 1 -or $owners[0] -ne [int]$Bridge.Ledger.pid) { return $false }
740
- return (Test-LocalReady $Cfg)
741
- }
742
- function Get-LocalHealthOwned($Cfg, $Bridge) {
743
- if (-not $Bridge -or -not $Bridge.Alive) { return [pscustomobject]@{ State='UNREACHABLE'; StatusCode=0; Diagnostic='bridge not alive' } }
744
- $owners = @(Get-LocalReadyListenerOwners $Cfg)
745
- if ($owners.Count -ne 1 -or $owners[0] -ne [int]$Bridge.Ledger.pid) { return [pscustomobject]@{ State='UNATTRIBUTED'; StatusCode=0; Diagnostic='readiness listener ownership mismatch' } }
746
- return (Get-LocalHealth $Cfg)
747
- }
748
-
749
- function Get-ObservedState($Cfg) {
750
- $bridge = Test-OwnedInstance 'Bridge'
751
- $cloud = Test-OwnedInstance 'Cloud'
752
- $core = Get-DownstreamCoreObservation $Cfg $bridge
753
- $health = if ($bridge.Alive -and $core.alive) { try { Get-LocalHealthOwned $Cfg $bridge } catch { [pscustomobject]@{ State='UNREACHABLE'; StatusCode=0; Diagnostic='readiness attribution failed' } } } else { [pscustomobject]@{ State='UNREACHABLE'; StatusCode=0; Diagnostic='core not alive' } }
754
- $ready = $health.State -eq 'READY'
755
- $state = if (-not $bridge.Alive -and -not $cloud.Alive) { 'OFFLINE' }
756
- elseif (-not $bridge.Alive -and $cloud.Alive) { 'PARKED' }
757
- elseif ($bridge.Alive -and $cloud.Alive -and $ready) { 'ONLINE' }
758
- elseif ($bridge.Alive -and $cloud.Alive -and $core.alive -and $health.State -eq 'DEGRADED') { 'DEGRADED' }
759
- elseif ($bridge.Alive -and $core.reason -eq 'startup-zero-child') { 'STARTING' }
760
- else { 'FAILED' }
761
- [pscustomobject]@{ State=$state; Bridge=$bridge; Core=$core; Cloud=$cloud; LocalReady=$ready; LocalHealth=$health }
762
- }
763
-
764
- function Wait-LocalOperational($Cfg) {
765
- $deadline = [DateTime]::UtcNow.AddSeconds([int]$Cfg.StartTimeoutSeconds)
766
- do {
767
- $bridge = Test-OwnedInstance 'Bridge'
768
- if (-not $bridge.Alive) { return $null }
769
- $core = Get-DownstreamCoreObservation $Cfg $bridge
770
- if (-not $core.alive -and $core.reason -ne 'startup-zero-child') { return $null }
771
- if ($core.alive) {
772
- $health = Get-LocalHealthOwned $Cfg $bridge
773
- if ($health.State -in @('READY','DEGRADED')) { return $health }
774
- }
775
- if ([DateTime]::UtcNow -ge $deadline) { return $null }
776
- Start-Sleep -Milliseconds 250
777
- } while ([DateTime]::UtcNow -lt $deadline)
778
- return $null
779
- }
780
-
781
- function Wait-LocalReady($Cfg) {
782
- $deadline = [DateTime]::UtcNow.AddSeconds([int]$Cfg.StartTimeoutSeconds)
783
- do {
784
- $bridge = Test-OwnedInstance 'Bridge'
785
- if (-not $bridge.Alive) { return $false }
786
- $core = Get-DownstreamCoreObservation $Cfg $bridge
787
- if (-not $core.alive -and $core.reason -ne 'startup-zero-child') { return $false }
788
- if ($core.alive -and (Test-LocalReadyOwned $Cfg $bridge)) { return $true }
789
- if ([DateTime]::UtcNow -ge $deadline) { return $false }
790
- Start-Sleep -Milliseconds 250
791
- } while ([DateTime]::UtcNow -lt $deadline)
792
- return $false
793
- }
794
-
795
- function Wait-PublicDnsReady($Cfg, [string]$Url) {
796
- try { $uri = [Uri]$Url } catch { Fail 'public MCP URL is invalid' }
797
- if ($uri.Scheme -ne 'https') { Fail 'public MCP URL must be HTTPS' }
798
- $deadline = [DateTime]::UtcNow.AddSeconds([int]$Cfg.StartTimeoutSeconds)
799
- $requiredConsecutive = 3
800
- $consecutive = 0
801
- do {
802
- try {
803
- $addresses = @([System.Net.Dns]::GetHostAddresses($uri.DnsSafeHost))
804
- if ($addresses.Count -gt 0) { $consecutive += 1 } else { $consecutive = 0 }
805
- } catch { $consecutive = 0 }
806
- if ($consecutive -ge $requiredConsecutive) { return }
807
- Start-Sleep -Milliseconds 750
808
- } while ([DateTime]::UtcNow -lt $deadline)
809
- Fail 'public endpoint DNS did not become stably resolvable before gate'
810
- }
811
-
812
- function Get-CurrentUrl($Cfg) {
813
- if ($Cfg.ContainsKey('McpUrlFile') -and -not [string]::IsNullOrWhiteSpace([string]$Cfg.McpUrlFile)) {
814
- $fullFile = [string]$Cfg.McpUrlFile
815
- if (-not (Test-Path -LiteralPath $fullFile -PathType Leaf)) { Fail "McpUrlFile missing: $fullFile" }
816
- $transport = if ($Cfg.Cloud.ContainsKey('Transport')) { [string]$Cfg.Cloud.Transport } else { '' }
817
- if ($transport -eq 'deadbyte.named-tunnel.v2') {
818
- if (-not $Cfg.ContainsKey('McpUrlFileSha256') -or [string]$Cfg.McpUrlFileSha256 -cnotmatch '^[0-9a-f]{64}$') { Fail 'deadbyte.named-tunnel.v2 requires McpUrlFileSha256' }
819
- $fullHash = Get-ExecutableHash $fullFile
820
- if ($fullHash -ne [string]$Cfg.McpUrlFileSha256) { Fail "McpUrlFile SHA-256 mismatch: expected $($Cfg.McpUrlFileSha256), found $fullHash" }
821
- }
822
- $full = (Read-TextFileSafe $fullFile).Trim()
823
- if ($full -notmatch '^https://') { Fail 'McpUrlFile must contain one HTTPS MCP endpoint' }
824
- if ($transport -eq 'deadbyte.named-tunnel.v2') {
825
- try { $fullUri = [System.Uri]$full } catch { Fail 'McpUrlFile must contain one valid HTTPS MCP endpoint' }
826
- if ($fullUri.DnsSafeHost.ToLowerInvariant().EndsWith('.trycloudflare.com')) { Fail 'trycloudflare endpoint forbidden for deadbyte.named-tunnel.v2' }
827
- }
828
- return $full
829
- }
830
- $cloud = Test-OwnedInstance 'Cloud'
831
- if (-not $cloud.Alive) { return $null }
832
- $originFile = Join-Path $RuntimeDir 'current-origin.txt'
833
- $origin = (Read-TextFileSafe $originFile).Trim()
834
- if (-not $origin) {
835
- $pattern = [string]$Cfg.UrlRegex
836
- foreach ($file in @((Log-Path 'Cloud' 'stdout'), (Log-Path 'Cloud' 'stderr'))) {
837
- $text = Read-TextFileSafe $file
838
- $m = [regex]::Matches($text, $pattern)
839
- if ($m.Count -gt 0) { $origin = $m[$m.Count - 1].Value }
840
- }
841
- if (-not $origin) { return $null }
842
- Set-Content -LiteralPath $originFile -Value $origin -Encoding ASCII
843
- }
844
- $suffix = ''
845
- if ($Cfg.ContainsKey('McpPathFile') -and -not [string]::IsNullOrWhiteSpace([string]$Cfg.McpPathFile)) {
846
- $pathFile = [string]$Cfg.McpPathFile
847
- if (-not (Test-Path -LiteralPath $pathFile -PathType Leaf)) { Fail "McpPathFile missing: $pathFile" }
848
- $suffix = (Read-TextFileSafe $pathFile).Trim()
849
- if ($suffix -and -not $suffix.StartsWith('/')) { Fail 'MCP path file must contain an absolute URL path beginning with slash' }
850
- if ($suffix -match '[\r\n]') { Fail 'MCP path file contains newline data' }
851
- }
852
- return $origin.TrimEnd('/') + $suffix
853
- }
854
-
855
- function Wait-CurrentUrl($Cfg) {
856
- $deadline = [DateTime]::UtcNow.AddSeconds([int]$Cfg.StartTimeoutSeconds)
857
- do {
858
- $cloud = Test-OwnedInstance 'Cloud'
859
- if (-not $cloud.Alive) { return $null }
860
- $url = Get-CurrentUrl $Cfg
861
- if ($url) { return $url }
862
- Start-Sleep -Milliseconds 250
863
- } while ([DateTime]::UtcNow -lt $deadline)
864
- return $null
865
- }
866
-
867
- function Invoke-CoreGate($Cfg, [string]$ScriptName, [string[]]$ExtraArguments = @()) {
868
- $coreRoot = [string]$Cfg.CoreRoot
869
- $script = Join-Path (Join-Path $coreRoot 'src') $ScriptName
870
- if (-not (Test-Path -LiteralPath $script -PathType Leaf)) { Fail "core gate script missing: $script" }
871
- $node = (Get-Command node.exe -ErrorAction Stop).Source
872
- $stdout = Log-Path 'CoreGate' 'stdout'
873
- $stderr = Log-Path 'CoreGate' 'stderr'
874
- Remove-Item -LiteralPath $stdout,$stderr -Force -ErrorAction SilentlyContinue
875
- $argv = @(('"{0}"' -f $script))
876
- foreach ($arg in $ExtraArguments) {
877
- if ($arg -match '[\r\n"]') { Fail 'core gate argument contains forbidden quoting/newline data' }
878
- $argv += ('"{0}"' -f $arg)
879
- }
880
- $proc = Start-Process -FilePath $node -ArgumentList $argv -WorkingDirectory $coreRoot -Wait -PassThru -WindowStyle Hidden -RedirectStandardOutput $stdout -RedirectStandardError $stderr
881
- if ($proc.ExitCode -ne 0) {
882
- $detail = if (Test-Path -LiteralPath $stderr) { (Get-Content -LiteralPath $stderr -Raw -Encoding UTF8).Trim() } else { '' }
883
- Fail "core gate $ScriptName failed exit=$($proc.ExitCode): $detail"
884
- }
885
- Write-Host (Get-Content -LiteralPath $stdout -Raw -Encoding UTF8)
886
- }
887
-
888
- function Start-Deadbyte($Cfg) {
889
- $state = Get-ObservedState $Cfg
890
- if ($state.State -in @('ONLINE','DEGRADED')) { return $state }
891
- if ($state.State -in @('FAILED','STARTING')) { Fail "runtime is $($state.State); inspect status and reconcile verified owned components before restart" }
892
- $cloudWasAlive = [bool]$state.Cloud.Alive
893
- Assert-LocalReadyPortAvailable $Cfg
894
- try {
895
- Start-OwnedComponent 'Bridge' $Cfg.Bridge $Cfg | Out-Null
896
- $localHealth = Wait-LocalOperational $Cfg
897
- if ($null -eq $localHealth) { Fail 'bridge started but owned local readiness gate failed' }
898
- Start-OwnedComponent 'Cloud' $Cfg.Cloud $Cfg | Out-Null
899
- if (-not (Wait-CurrentUrl $Cfg)) { Fail 'public endpoint metadata did not become available before timeout' }
900
- $final = Get-ObservedState $Cfg
901
- if ($final.State -notin @('ONLINE','DEGRADED')) { Fail "start gate failed: observed $($final.State)" }
902
- return $final
903
- } catch {
904
- try { Stop-OwnedComponent 'Bridge' } catch {}
905
- if (-not $cloudWasAlive) { try { Stop-OwnedComponent 'Cloud' } catch {} }
906
- throw
907
- }
908
- }
909
-
910
- function Park-Deadbyte($Cfg) {
911
- Disarm-Desktop $Cfg -Quiet
912
- Stop-OwnedComponent 'Bridge'
913
- $s = Get-ObservedState $Cfg
914
- if ($s.State -notin @('PARKED','OFFLINE')) { Fail "park gate failed: observed $($s.State)" }
915
- return $s
916
- }
917
-
918
- function Stop-Deadbyte($Cfg) {
919
- Disarm-Desktop $Cfg -Quiet
920
- Stop-OwnedComponent 'Bridge'
921
- Stop-OwnedComponent 'Cloud'
922
- Remove-Item -LiteralPath (Join-Path $RuntimeDir 'current-origin.txt') -Force -ErrorAction SilentlyContinue
923
- $s = Get-ObservedState $Cfg
924
- if ($s.State -ne 'OFFLINE') { Fail "stop gate failed: observed $($s.State)" }
925
- return $s
926
- }
927
-
928
- function Assert-PolicyRootsAvailable($Policy, [string]$Plane) {
929
- $rootIds = @($Policy.roots.PSObject.Properties.Name)
930
- if ($rootIds.Count -lt 1) { Fail "$Plane policy requires at least one root" }
931
- foreach ($rootId in $rootIds) {
932
- $raw = [string]$Policy.roots.$rootId.path
933
- if ([string]::IsNullOrWhiteSpace($raw)) { Fail "$Plane policy root '$rootId' path missing" }
934
- try { $full = [System.IO.Path]::GetFullPath($raw) } catch { Fail "$Plane policy root '$rootId' path invalid" }
935
- if (-not (Test-Path -LiteralPath $full -PathType Container)) { Fail "$Plane policy root '$rootId' missing: $full" }
936
- }
937
- return $rootIds
938
- }
939
-
940
- function Get-AuthorityEpochStatus($Cfg, [string]$Plane, [string]$GrantFile, [string]$GrantNonce) {
941
- if ([string]::IsNullOrWhiteSpace($GrantNonce) -or $GrantNonce -cnotmatch '^[0-9a-f]{64}$') {
942
- return [pscustomobject]@{ Trusted=$false; Reason='epoch-grant-nonce-invalid'; Seq=$null; Action=$null; EventSha256=$null }
943
- }
944
- try {
945
- $verified = Invoke-AuthorityEpochCommand $Cfg $Plane 'status' $GrantFile
946
- if (-not $verified.trusted) {
947
- return [pscustomobject]@{ Trusted=$false; Reason=[string]$verified.reason; Seq=$verified.seq; Action=$verified.action; EventSha256=$verified.event_sha256 }
948
- }
949
- return [pscustomobject]@{ Trusted=$true; Reason=[string]$verified.reason; Seq=$verified.seq; Action=$verified.action; EventSha256=$verified.event_sha256 }
950
- } catch {
951
- return [pscustomobject]@{ Trusted=$false; Reason='authority-epoch-verifier-unavailable'; Seq=$null; Action=$null; EventSha256=$null }
952
- }
953
- }
954
-
955
- function Invoke-AuthorityEpochCommand($Cfg, [string]$Plane, [string]$Action, [string]$GrantFile, [string]$UpdateId = '', [string]$ExpectedHead = '') {
956
- $cli = Join-Path ([string]$Cfg.CoreRoot) 'src\authority-epoch-cli-r65.mjs'
957
- if (-not (Test-Path -LiteralPath $cli -PathType Leaf)) { Fail "authority epoch CLI missing: $cli" }
958
- $node = [string]$Cfg.Bridge.FilePath
959
- if ([string]::IsNullOrWhiteSpace($node) -or -not (Test-Path -LiteralPath $node -PathType Leaf)) { Fail 'authority epoch Node executable unavailable' }
960
- $policyFile = switch ($Plane) {
961
- 'process' { [string]$Cfg.ProcessPolicyFile }
962
- 'coding' { [string]$Cfg.CodingPolicyFile }
963
- 'autonomous' { [string]$Cfg.AutonomousPolicyFile }
964
- 'desktop' { [string]$Cfg.DesktopPolicyFile }
965
- 'host' { [string]$Cfg.HostPolicyFile }
966
- default { Fail "authority epoch plane invalid: $Plane" }
967
- }
968
- if ([string]::IsNullOrWhiteSpace($policyFile) -or -not (Test-Path -LiteralPath $policyFile -PathType Leaf)) { Fail "authority epoch policy unavailable for $Plane" }
969
- $args = @($cli, $Action, '--plane', $Plane, '--grant-file', $GrantFile, '--policy-file', $policyFile)
970
- if ($Action -in @('transaction-revoke','transaction-restore')) {
971
- if ([string]::IsNullOrWhiteSpace($UpdateId) -or $ExpectedHead -cnotmatch '^[0-9a-f]{64}$') { Fail 'authority epoch transaction binding invalid' }
972
- $args += @('--update-id', $UpdateId, '--expected-head', $ExpectedHead)
973
- }
974
- $output = @(& $node @args)
975
- $exit = $LASTEXITCODE
976
- if ($exit -ne 0) { Fail "authority epoch command '$Action' failed for $Plane (exit=$exit)" }
977
- try { return (($output -join [Environment]::NewLine) | ConvertFrom-Json) }
978
- catch { Fail "authority epoch command '$Action' returned invalid JSON for $Plane" }
979
- }
980
-
981
- function Get-HostPolicyMetadata($Cfg) {
982
- if ([string]::IsNullOrWhiteSpace([string]$Cfg.HostPolicyFile)) { Fail 'privileged host plane is not configured' }
983
- $policyPath = [string]$Cfg.HostPolicyFile
984
- try { $policy = Get-Content -LiteralPath $policyPath -Raw -Encoding UTF8 | ConvertFrom-Json } catch { Fail "invalid host policy JSON: $policyPath" }
985
- if (@('deadbyte.host-policy.v1','deadbyte.host-policy.v2') -notcontains [string]$policy.schema -or $policy.enabled -ne $true) { Fail 'host policy schema/enabled mismatch' }
986
- if ([string]::IsNullOrWhiteSpace([string]$policy.arm_file)) { Fail 'host policy arm_file missing' }
987
- $armFile = [System.IO.Path]::GetFullPath([string]$policy.arm_file)
988
- $runtimeReal = [System.IO.Path]::GetFullPath($RuntimeDir).TrimEnd('\') + '\'
989
- if (-not $armFile.StartsWith($runtimeReal, [System.StringComparison]::OrdinalIgnoreCase)) { Fail 'host policy arm_file must live under controller runtime directory' }
990
- Assert-PolicyRootsAvailable $policy 'host' | Out-Null
991
- [pscustomobject]@{
992
- Path = $policyPath
993
- Sha256 = Get-ExecutableHash $policyPath
994
- ArmFile = $armFile
995
- }
996
- }
997
-
998
- function Get-HostArmStatus($Cfg) {
999
- if ([string]::IsNullOrWhiteSpace([string]$Cfg.HostPolicyFile)) { return [pscustomobject]@{ Configured=$false; Armed=$false; Reason='no-policy' } }
1000
- try { $meta = Get-HostPolicyMetadata $Cfg } catch { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason=$_.Exception.Message } }
1001
- if (-not (Test-Path -LiteralPath $meta.ArmFile -PathType Leaf)) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='disarmed' } }
1002
- try { $arm = Get-Content -LiteralPath $meta.ArmFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='invalid-grant' } }
1003
- if ($arm.schema -ne 'deadbyte.host-grant.v2') { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='grant-schema' } }
1004
- if ([string]$arm.policy_sha256 -ne [string]$meta.Sha256) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='policy-digest' } }
1005
- if ([string]$arm.instance_nonce -cnotmatch '^[0-9a-f]{64}$') { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='nonce-invalid' } }
1006
- $epoch = Get-AuthorityEpochStatus $Cfg 'host' $meta.ArmFile ([string]$arm.instance_nonce)
1007
- if (-not $epoch.Trusted) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason=[string]$epoch.Reason } }
1008
- return [pscustomobject]@{ Configured=$true; Armed=$true; Reason='standing-grant'; EpochSeq=$epoch.Seq; EpochSha256=$epoch.EventSha256 }
1009
- }
1010
-
1011
- function Arm-Host($Cfg) {
1012
- $meta = Get-HostPolicyMetadata $Cfg
1013
- Ensure-Directory $RuntimeDir
1014
- $nonceBytes = New-Object byte[] 32
1015
- $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
1016
- try { $rng.GetBytes($nonceBytes) } finally { $rng.Dispose() }
1017
- $nonce = -join ($nonceBytes | ForEach-Object { $_.ToString('x2') })
1018
- $now = [DateTime]::UtcNow
1019
- $body = [ordered]@{
1020
- schema = 'deadbyte.host-grant.v2'
1021
- policy_sha256 = [string]$meta.Sha256
1022
- instance_nonce = $nonce
1023
- granted_at_utc = $now.ToString('o')
1024
- }
1025
- $tmp = $meta.ArmFile + '.tmp'
1026
- $body | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $tmp -Encoding UTF8
1027
- Move-Item -LiteralPath $tmp -Destination $meta.ArmFile -Force
1028
- try { Invoke-AuthorityEpochCommand $Cfg 'host' 'grant' $meta.ArmFile | Out-Null }
1029
- catch {
1030
- Remove-Item -LiteralPath $meta.ArmFile -Force -ErrorAction SilentlyContinue
1031
- throw
1032
- }
1033
- Write-Host 'PRIVILEGED HOST PLANE ENABLED UNTIL EXPLICIT REVOKE'
1034
- }
1035
-
1036
- function Disarm-Host($Cfg) {
1037
- $meta = Get-HostPolicyMetadata $Cfg
1038
- $epochError = $null
1039
- if (Test-Path -LiteralPath $meta.ArmFile) {
1040
- try { Invoke-AuthorityEpochCommand $Cfg 'host' 'user-revoke' $meta.ArmFile | Out-Null } catch { $epochError = $_.Exception }
1041
- Remove-Item -LiteralPath $meta.ArmFile -Force -ErrorAction SilentlyContinue
1042
- }
1043
- if (Test-Path -LiteralPath $meta.ArmFile) { throw 'Host authority revocation failed: grant remains present' }
1044
- if ($null -ne $epochError) { throw $epochError }
1045
- Write-Host 'PRIVILEGED HOST PLANE DISARMED'
1046
- }
1047
-
1048
- function Show-HostStatus($Cfg) {
1049
- $h = Get-HostArmStatus $Cfg
1050
- Write-Host ("Host plane : {0} [{1}]" -f $(if ($h.Armed) { 'ARMED' } elseif ($h.Configured) { 'DISARMED' } else { 'UNCONFIGURED' }), $h.Reason)
1051
- return $h
1052
- }
1053
-
1054
- function Get-ProcessPolicyMetadata($Cfg) {
1055
- if ([string]::IsNullOrWhiteSpace([string]$Cfg.ProcessPolicyFile)) { Fail 'process plane is not configured' }
1056
- $policyPath = [string]$Cfg.ProcessPolicyFile
1057
- try { $policy = Get-Content -LiteralPath $policyPath -Raw -Encoding UTF8 | ConvertFrom-Json } catch { Fail "invalid process policy JSON: $policyPath" }
1058
- if ($policy.schema -ne 'deadbyte.process-policy.v1' -or $policy.enabled -ne $true) { Fail 'process policy schema/enabled mismatch' }
1059
- if ([string]::IsNullOrWhiteSpace([string]$policy.grant_file)) { Fail 'process policy grant_file missing' }
1060
- $grantFile = [System.IO.Path]::GetFullPath([string]$policy.grant_file)
1061
- $runtimeReal = [System.IO.Path]::GetFullPath($RuntimeDir).TrimEnd('\') + '\'
1062
- if (-not $grantFile.StartsWith($runtimeReal, [System.StringComparison]::OrdinalIgnoreCase)) { Fail 'process policy grant_file must live under controller runtime directory' }
1063
- $profileIds = @($policy.profiles.PSObject.Properties.Name)
1064
- $rootIds = @(Assert-PolicyRootsAvailable $policy 'process')
1065
- if ($profileIds.Count -lt 1 -or $rootIds.Count -lt 1) { Fail 'process policy requires profiles and roots' }
1066
- [pscustomobject]@{ Path=$policyPath; Sha256=Get-ExecutableHash $policyPath; GrantFile=$grantFile; ProfileIds=$profileIds; RootIds=$rootIds }
1067
- }
1068
-
1069
- function Get-ProcessGrantStatus($Cfg) {
1070
- if ([string]::IsNullOrWhiteSpace([string]$Cfg.ProcessPolicyFile)) { return [pscustomobject]@{ Configured=$false; Armed=$false; Reason='no-policy' } }
1071
- try { $meta = Get-ProcessPolicyMetadata $Cfg } catch { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason=$_.Exception.Message } }
1072
- if (-not (Test-Path -LiteralPath $meta.GrantFile -PathType Leaf)) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='disarmed' } }
1073
- try { $grant = Get-Content -LiteralPath $meta.GrantFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='invalid-grant' } }
1074
- if ($grant.schema -ne 'deadbyte.process-grant.v1') { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='grant-schema' } }
1075
- if ([string]$grant.policy_sha256 -ne [string]$meta.Sha256) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='policy-digest' } }
1076
- if ([string]$grant.instance_nonce -cnotmatch '^[0-9a-f]{64}$') { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='nonce-invalid' } }
1077
- $profiles = @($grant.profile_ids); $roots = @($grant.root_ids)
1078
- if ($profiles.Count -lt 1 -or @($profiles | Where-Object { $_ -notin $meta.ProfileIds }).Count -gt 0) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='profiles-invalid' } }
1079
- if ($roots.Count -lt 1 -or @($roots | Where-Object { $_ -notin $meta.RootIds }).Count -gt 0) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='roots-invalid' } }
1080
- $epoch = Get-AuthorityEpochStatus $Cfg 'process' $meta.GrantFile ([string]$grant.instance_nonce)
1081
- if (-not $epoch.Trusted) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason=[string]$epoch.Reason } }
1082
- return [pscustomobject]@{ Configured=$true; Armed=$true; Reason='standing-grant'; EpochSeq=$epoch.Seq; EpochSha256=$epoch.EventSha256 }
1083
- }
1084
-
1085
- function Arm-Process($Cfg) {
1086
- $meta = Get-ProcessPolicyMetadata $Cfg
1087
- Ensure-Directory $RuntimeDir
1088
- $nonceBytes = New-Object byte[] 32
1089
- $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
1090
- try { $rng.GetBytes($nonceBytes) } finally { $rng.Dispose() }
1091
- $nonce = -join ($nonceBytes | ForEach-Object { $_.ToString('x2') })
1092
- $body = [ordered]@{
1093
- schema = 'deadbyte.process-grant.v1'
1094
- policy_sha256 = [string]$meta.Sha256
1095
- instance_nonce = $nonce
1096
- profile_ids = @($meta.ProfileIds)
1097
- root_ids = @($meta.RootIds)
1098
- granted_at_utc = [DateTime]::UtcNow.ToString('o')
1099
- }
1100
- $tmp = $meta.GrantFile + '.tmp'
1101
- $body | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $tmp -Encoding UTF8
1102
- Move-Item -LiteralPath $tmp -Destination $meta.GrantFile -Force
1103
- try { Invoke-AuthorityEpochCommand $Cfg 'process' 'grant' $meta.GrantFile | Out-Null }
1104
- catch {
1105
- Remove-Item -LiteralPath $meta.GrantFile -Force -ErrorAction SilentlyContinue
1106
- throw
1107
- }
1108
- Write-Host 'PROCESS PLANE ENABLED UNTIL EXPLICIT REVOKE'
1109
- }
1110
-
1111
- function Disarm-Process($Cfg) {
1112
- $meta = Get-ProcessPolicyMetadata $Cfg
1113
- $epochError = $null
1114
- if (Test-Path -LiteralPath $meta.GrantFile -PathType Leaf) {
1115
- try { Invoke-AuthorityEpochCommand $Cfg 'process' 'user-revoke' $meta.GrantFile | Out-Null } catch { $epochError = $_.Exception }
1116
- Remove-Item -LiteralPath $meta.GrantFile -Force -ErrorAction SilentlyContinue
1117
- }
1118
- if (Test-Path -LiteralPath $meta.GrantFile) { throw 'Process authority revocation failed: grant remains present' }
1119
- if ($null -ne $epochError) { throw $epochError }
1120
- Write-Host 'PROCESS PLANE DISARMED'
1121
- }
1122
-
1123
- function Show-ProcessStatus($Cfg) {
1124
- $p = Get-ProcessGrantStatus $Cfg
1125
- Write-Host ("Process plane : {0} [{1}]" -f $(if ($p.Armed) { 'ARMED' } elseif ($p.Configured) { 'DISARMED' } else { 'UNCONFIGURED' }), $p.Reason)
1126
- return $p
1127
- }
1128
-
1129
- function Get-CodingPolicyMetadata($Cfg) {
1130
- if ([string]::IsNullOrWhiteSpace([string]$Cfg.CodingPolicyFile)) { Fail 'coding plane is not configured' }
1131
- $policyPath = [string]$Cfg.CodingPolicyFile
1132
- try { $policy = Get-Content -LiteralPath $policyPath -Raw -Encoding UTF8 | ConvertFrom-Json } catch { Fail "invalid coding policy JSON: $policyPath" }
1133
- if ($policy.schema -ne 'deadbyte.coding-policy.v1' -or $policy.enabled -ne $true) { Fail 'coding policy schema/enabled mismatch' }
1134
- if ([string]::IsNullOrWhiteSpace([string]$policy.lease_file)) { Fail 'coding policy lease_file missing' }
1135
- $leaseFile = [System.IO.Path]::GetFullPath([string]$policy.lease_file)
1136
- $runtimeReal = [System.IO.Path]::GetFullPath($RuntimeDir).TrimEnd('\') + '\'
1137
- if (-not $leaseFile.StartsWith($runtimeReal, [System.StringComparison]::OrdinalIgnoreCase)) { Fail 'coding policy lease_file must live under controller runtime directory' }
1138
- $rootIds = @(Assert-PolicyRootsAvailable $policy 'coding')
1139
- [pscustomobject]@{ Path=$policyPath; Sha256=Get-ExecutableHash $policyPath; LeaseFile=$leaseFile; RootIds=$rootIds }
1140
- }
1141
-
1142
- function Get-CodingLeaseStatus($Cfg) {
1143
- if ([string]::IsNullOrWhiteSpace([string]$Cfg.CodingPolicyFile)) { return [pscustomobject]@{ Configured=$false; Armed=$false; Reason='no-policy' } }
1144
- try { $meta = Get-CodingPolicyMetadata $Cfg } catch { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason=$_.Exception.Message } }
1145
- if (-not (Test-Path -LiteralPath $meta.LeaseFile -PathType Leaf)) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='disarmed' } }
1146
- try { $lease = Get-Content -LiteralPath $meta.LeaseFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='invalid-grant' } }
1147
- if ($lease.schema -ne 'deadbyte.coding-grant.v2') { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='grant-schema' } }
1148
- if ([string]$lease.policy_sha256 -ne [string]$meta.Sha256) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='policy-digest' } }
1149
- if ([string]$lease.instance_nonce -cnotmatch '^[0-9a-f]{64}$') { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='nonce-invalid' } }
1150
- $roots = @($lease.root_ids)
1151
- if ($roots.Count -lt 1 -or @($roots | Where-Object { $_ -notin $meta.RootIds }).Count -gt 0) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='roots-invalid' } }
1152
- $epoch = Get-AuthorityEpochStatus $Cfg 'coding' $meta.LeaseFile ([string]$lease.instance_nonce)
1153
- if (-not $epoch.Trusted) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason=[string]$epoch.Reason } }
1154
- return [pscustomobject]@{ Configured=$true; Armed=$true; Reason='standing-grant'; EpochSeq=$epoch.Seq; EpochSha256=$epoch.EventSha256 }
1155
- }
1156
-
1157
- function Arm-Coding($Cfg) {
1158
- $meta = Get-CodingPolicyMetadata $Cfg
1159
- Ensure-Directory $RuntimeDir
1160
- $nonceBytes = New-Object byte[] 32
1161
- $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
1162
- try { $rng.GetBytes($nonceBytes) } finally { $rng.Dispose() }
1163
- $nonce = -join ($nonceBytes | ForEach-Object { $_.ToString('x2') })
1164
- $now = [DateTime]::UtcNow
1165
- $body = [ordered]@{
1166
- schema = 'deadbyte.coding-grant.v2'
1167
- policy_sha256 = [string]$meta.Sha256
1168
- instance_nonce = $nonce
1169
- root_ids = @($meta.RootIds)
1170
- granted_at_utc = $now.ToString('o')
1171
- }
1172
- $tmp = $meta.LeaseFile + '.tmp'
1173
- $body | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $tmp -Encoding UTF8
1174
- Move-Item -LiteralPath $tmp -Destination $meta.LeaseFile -Force
1175
- try { Invoke-AuthorityEpochCommand $Cfg 'coding' 'grant' $meta.LeaseFile | Out-Null }
1176
- catch {
1177
- Remove-Item -LiteralPath $meta.LeaseFile -Force -ErrorAction SilentlyContinue
1178
- throw
1179
- }
1180
- Write-Host 'CODING PLANE ENABLED UNTIL EXPLICIT REVOKE'
1181
- }
1182
-
1183
- function Disarm-Coding($Cfg) {
1184
- $meta = Get-CodingPolicyMetadata $Cfg
1185
- $epochError = $null
1186
- if (Test-Path -LiteralPath $meta.LeaseFile -PathType Leaf) {
1187
- try { Invoke-AuthorityEpochCommand $Cfg 'coding' 'user-revoke' $meta.LeaseFile | Out-Null } catch { $epochError = $_.Exception }
1188
- Remove-Item -LiteralPath $meta.LeaseFile -Force -ErrorAction SilentlyContinue
1189
- }
1190
- try { Disarm-Autonomous $Cfg -Quiet } catch {
1191
- if ($null -eq $epochError) { $epochError = $_.Exception }
1192
- else { Write-Warning "autonomous lease cleanup failed after coding revocation: $($_.Exception.Message)" }
1193
- }
1194
- if (Test-Path -LiteralPath $meta.LeaseFile) { throw 'Coding authority revocation failed: grant remains present' }
1195
- if ($null -ne $epochError) { throw $epochError }
1196
- Write-Host 'CODING PLANE DISARMED; AUTONOMOUS AUTHORITY REVOKED'
1197
- }
1198
-
1199
- function Show-CodingStatus($Cfg) {
1200
- $c = Get-CodingLeaseStatus $Cfg
1201
- Write-Host ("Coding plane : {0} [{1}]" -f $(if ($c.Armed) { 'ARMED' } elseif ($c.Configured) { 'DISARMED' } else { 'UNCONFIGURED' }), $c.Reason)
1202
- return $c
1203
- }
1204
-
1205
- function Get-AutonomousPolicyMetadata($Cfg) {
1206
- if ([string]::IsNullOrWhiteSpace([string]$Cfg.AutonomousPolicyFile)) { Fail 'autonomous plane is not configured' }
1207
- $policyPath = [string]$Cfg.AutonomousPolicyFile
1208
- try { $policy = Get-Content -LiteralPath $policyPath -Raw -Encoding UTF8 | ConvertFrom-Json } catch { Fail "invalid autonomous policy JSON: $policyPath" }
1209
- if ($policy.schema -ne 'deadbyte.autonomous-policy.v1' -or $policy.enabled -ne $true) { Fail 'autonomous policy schema/enabled mismatch' }
1210
- if ([string]::IsNullOrWhiteSpace([string]$policy.lease_file)) { Fail 'autonomous policy lease_file missing' }
1211
- $leaseFile = [System.IO.Path]::GetFullPath([string]$policy.lease_file)
1212
- $runtimeReal = [System.IO.Path]::GetFullPath($RuntimeDir).TrimEnd('\') + '\'
1213
- if (-not $leaseFile.StartsWith($runtimeReal, [System.StringComparison]::OrdinalIgnoreCase)) { Fail 'autonomous policy lease_file must live under controller runtime directory' }
1214
- $codingMeta = Get-CodingPolicyMetadata $Cfg
1215
- if ([string]$policy.coding_policy_sha256 -ne [string]$codingMeta.Sha256) { Fail 'autonomous policy coding_policy_sha256 mismatch' }
1216
- $rootIds = @($policy.root_ids)
1217
- if ($rootIds.Count -lt 1) { Fail 'autonomous policy requires at least one root' }
1218
- [pscustomobject]@{ Path=$policyPath; Sha256=Get-ExecutableHash $policyPath; LeaseFile=$leaseFile; RootIds=$rootIds; CodingPolicySha256=$codingMeta.Sha256 }
1219
- }
1220
-
1221
- function Get-AutonomousLeaseStatus($Cfg) {
1222
- if ([string]::IsNullOrWhiteSpace([string]$Cfg.AutonomousPolicyFile)) { return [pscustomobject]@{ Configured=$false; Armed=$false; Reason='no-policy' } }
1223
- try { $meta = Get-AutonomousPolicyMetadata $Cfg } catch { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason=$_.Exception.Message } }
1224
- $coding = Get-CodingLeaseStatus $Cfg
1225
- if (-not $coding.Armed) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='coding-disarmed' } }
1226
- if (-not (Test-Path -LiteralPath $meta.LeaseFile -PathType Leaf)) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='disarmed' } }
1227
- try { $lease = Get-Content -LiteralPath $meta.LeaseFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='invalid-grant' } }
1228
- if ($lease.schema -ne 'deadbyte.autonomous-grant.v2') { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='grant-schema' } }
1229
- if ([string]$lease.policy_sha256 -ne [string]$meta.Sha256) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='policy-digest' } }
1230
- if ([string]$lease.coding_policy_sha256 -ne [string]$meta.CodingPolicySha256) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='coding-policy-digest' } }
1231
- if ([string]$lease.instance_nonce -cnotmatch '^[0-9a-f]{64}$') { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='nonce-invalid' } }
1232
- $roots = @($lease.root_ids)
1233
- if ($roots.Count -lt 1 -or @($roots | Where-Object { $_ -notin $meta.RootIds }).Count -gt 0) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='roots-invalid' } }
1234
- $epoch = Get-AuthorityEpochStatus $Cfg 'autonomous' $meta.LeaseFile ([string]$lease.instance_nonce)
1235
- if (-not $epoch.Trusted) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason=[string]$epoch.Reason } }
1236
- return [pscustomobject]@{ Configured=$true; Armed=$true; Reason='standing-grant'; EpochSeq=$epoch.Seq; EpochSha256=$epoch.EventSha256 }
1237
- }
1238
-
1239
- function Arm-Autonomous($Cfg) {
1240
- $coding = Get-CodingLeaseStatus $Cfg
1241
- if (-not $coding.Armed) { Fail 'autonomous plane requires an active coding lease; run armcoding first' }
1242
- $meta = Get-AutonomousPolicyMetadata $Cfg
1243
- Ensure-Directory $RuntimeDir
1244
- $nonceBytes = New-Object byte[] 32
1245
- $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
1246
- try { $rng.GetBytes($nonceBytes) } finally { $rng.Dispose() }
1247
- $nonce = -join ($nonceBytes | ForEach-Object { $_.ToString('x2') })
1248
- $now = [DateTime]::UtcNow
1249
- $body = [ordered]@{ schema='deadbyte.autonomous-grant.v2'; policy_sha256=[string]$meta.Sha256; coding_policy_sha256=[string]$meta.CodingPolicySha256; instance_nonce=$nonce; root_ids=@($meta.RootIds); granted_at_utc=$now.ToString('o') }
1250
- $tmp = $meta.LeaseFile + '.tmp'
1251
- $body | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $tmp -Encoding UTF8
1252
- Move-Item -LiteralPath $tmp -Destination $meta.LeaseFile -Force
1253
- try { Invoke-AuthorityEpochCommand $Cfg 'autonomous' 'grant' $meta.LeaseFile | Out-Null }
1254
- catch {
1255
- Remove-Item -LiteralPath $meta.LeaseFile -Force -ErrorAction SilentlyContinue
1256
- throw
1257
- }
1258
- Write-Host 'AUTONOMOUS PLANE ENABLED UNTIL EXPLICIT REVOKE'
1259
- }
1260
-
1261
- function Disarm-Autonomous($Cfg, [switch]$Quiet) {
1262
- if ([string]::IsNullOrWhiteSpace([string]$Cfg.AutonomousPolicyFile)) { return }
1263
- $meta = Get-AutonomousPolicyMetadata $Cfg
1264
- $epochError = $null
1265
- if (Test-Path -LiteralPath $meta.LeaseFile -PathType Leaf) {
1266
- try { Invoke-AuthorityEpochCommand $Cfg 'autonomous' 'user-revoke' $meta.LeaseFile | Out-Null } catch { $epochError = $_.Exception }
1267
- Remove-Item -LiteralPath $meta.LeaseFile -Force -ErrorAction SilentlyContinue
1268
- }
1269
- if (Test-Path -LiteralPath $meta.LeaseFile) { throw 'Autonomous authority revocation failed: grant remains present' }
1270
- if ($null -ne $epochError) { throw $epochError }
1271
- if (-not $Quiet) { Write-Host 'AUTONOMOUS PLANE DISARMED' }
1272
- }
1273
-
1274
- function Show-AutonomousStatus($Cfg) {
1275
- $a = Get-AutonomousLeaseStatus $Cfg
1276
- Write-Host ("Autonomous : {0} [{1}]" -f $(if ($a.Armed) { 'ARMED' } elseif ($a.Configured) { 'DISARMED' } else { 'UNCONFIGURED' }), $a.Reason)
1277
- return $a
1278
- }
1279
-
1280
- function Get-DesktopPolicyMetadata($Cfg) {
1281
- if ([string]::IsNullOrWhiteSpace([string]$Cfg.DesktopPolicyFile)) { Fail 'desktop plane is not configured' }
1282
- $policyPath = [string]$Cfg.DesktopPolicyFile
1283
- try { $policy = Get-Content -LiteralPath $policyPath -Raw -Encoding UTF8 | ConvertFrom-Json } catch { Fail "invalid desktop policy JSON: $policyPath" }
1284
- if ($policy.schema -ne 'deadbyte.desktop-policy.v1' -or $policy.enabled -ne $true) { Fail 'desktop policy schema/enabled mismatch' }
1285
- if ([string]::IsNullOrWhiteSpace([string]$policy.grant_file)) { Fail 'desktop policy grant_file missing' }
1286
- if ([string]::IsNullOrWhiteSpace([string]$policy.evidence_dir)) { Fail 'desktop policy evidence_dir missing' }
1287
- $grantFile = [System.IO.Path]::GetFullPath([string]$policy.grant_file)
1288
- $evidenceDir = [System.IO.Path]::GetFullPath([string]$policy.evidence_dir)
1289
- $runtimeReal = [System.IO.Path]::GetFullPath($RuntimeDir).TrimEnd('\') + '\'
1290
- if (-not $grantFile.StartsWith($runtimeReal, [System.StringComparison]::OrdinalIgnoreCase)) { Fail 'desktop policy grant_file must live under controller runtime directory' }
1291
- if (-not ($evidenceDir.TrimEnd('\') + '\').StartsWith($runtimeReal, [System.StringComparison]::OrdinalIgnoreCase)) { Fail 'desktop policy evidence_dir must live under controller runtime directory' }
1292
- $knownActions = @('pointer_move','click_primary','click_secondary','double_click_primary','scroll','key_chord','text_input')
1293
- $actionKinds = @($policy.allowed_actions)
1294
- $uniqueActions = @($actionKinds | Sort-Object -Unique)
1295
- if ($actionKinds.Count -lt 1 -or $actionKinds.Count -ne $uniqueActions.Count -or @($actionKinds | Where-Object { $_ -notin $knownActions }).Count -gt 0) { Fail 'desktop policy allowed_actions invalid' }
1296
- if (-not $policy.observer -or [string]::IsNullOrWhiteSpace([string]$policy.observer.executable) -or [string]$policy.observer.sha256 -cnotmatch '^[0-9a-f]{64}$') { Fail 'desktop observer metadata invalid' }
1297
- if (-not $policy.input -or [string]::IsNullOrWhiteSpace([string]$policy.input.executable) -or [string]$policy.input.sha256 -cnotmatch '^[0-9a-f]{64}$') { Fail 'desktop input metadata invalid' }
1298
- if (-not $policy.grounder -or [string]::IsNullOrWhiteSpace([string]$policy.grounder.executable) -or [string]$policy.grounder.sha256 -cnotmatch '^[0-9a-f]{64}$') { Fail 'desktop grounder metadata invalid' }
1299
- $observerPath = [System.IO.Path]::GetFullPath([string]$policy.observer.executable)
1300
- $inputPath = [System.IO.Path]::GetFullPath([string]$policy.input.executable)
1301
- $grounderPath = [System.IO.Path]::GetFullPath([string]$policy.grounder.executable)
1302
- foreach ($provider in @(
1303
- [pscustomobject]@{ Name='observer'; Path=$observerPath; Sha256=([string]$policy.observer.sha256).ToLowerInvariant() },
1304
- [pscustomobject]@{ Name='input'; Path=$inputPath; Sha256=([string]$policy.input.sha256).ToLowerInvariant() },
1305
- [pscustomobject]@{ Name='grounder'; Path=$grounderPath; Sha256=([string]$policy.grounder.sha256).ToLowerInvariant() }
1306
- )) {
1307
- if (-not (Test-Path -LiteralPath $provider.Path -PathType Leaf)) { Fail "desktop $($provider.Name) executable missing: $($provider.Path)" }
1308
- $item = Get-Item -LiteralPath $provider.Path -Force
1309
- if (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { Fail "desktop $($provider.Name) executable must not be a reparse point" }
1310
- $actual = Get-ExecutableHash $provider.Path
1311
- if ($actual -ne $provider.Sha256) { Fail "desktop $($provider.Name) executable SHA-256 mismatch: expected $($provider.Sha256), found $actual" }
1312
- }
1313
- [pscustomobject]@{
1314
- Path=$policyPath; Sha256=Get-ExecutableHash $policyPath; GrantFile=$grantFile; EvidenceDir=$evidenceDir;
1315
- ActionKinds=$actionKinds; ObserverPath=$observerPath; ObserverSha256=([string]$policy.observer.sha256).ToLowerInvariant();
1316
- InputPath=$inputPath; InputSha256=([string]$policy.input.sha256).ToLowerInvariant();
1317
- GrounderPath=$grounderPath; GrounderSha256=([string]$policy.grounder.sha256).ToLowerInvariant()
1318
- }
1319
- }
1320
-
1321
- function Get-DesktopGrantFileForRevoke($Cfg) {
1322
- if ([string]::IsNullOrWhiteSpace([string]$Cfg.DesktopPolicyFile)) { return '' }
1323
- $policyPath = [string]$Cfg.DesktopPolicyFile
1324
- try { $policy = Get-Content -LiteralPath $policyPath -Raw -Encoding UTF8 | ConvertFrom-Json } catch { Fail "invalid desktop policy JSON during revoke: $policyPath" }
1325
- if ([string]::IsNullOrWhiteSpace([string]$policy.grant_file)) { Fail 'desktop policy grant_file missing during revoke' }
1326
- $grantFile = [System.IO.Path]::GetFullPath([string]$policy.grant_file)
1327
- $runtimeReal = [System.IO.Path]::GetFullPath($RuntimeDir).TrimEnd('\') + '\'
1328
- if (-not $grantFile.StartsWith($runtimeReal, [System.StringComparison]::OrdinalIgnoreCase)) { Fail 'desktop policy grant_file must live under controller runtime directory' }
1329
- return $grantFile
1330
- }
1331
-
1332
- function Get-DesktopGrantStatus($Cfg) {
1333
- if ([string]::IsNullOrWhiteSpace([string]$Cfg.DesktopPolicyFile)) { return [pscustomobject]@{ Configured=$false; Armed=$false; Reason='no-policy'; ActionKinds=@() } }
1334
- try { $meta = Get-DesktopPolicyMetadata $Cfg } catch { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason=$_.Exception.Message; ActionKinds=@() } }
1335
- if (-not (Test-Path -LiteralPath $meta.GrantFile -PathType Leaf)) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='disarmed'; ActionKinds=@() } }
1336
- try { $grant = Get-Content -LiteralPath $meta.GrantFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='invalid-grant'; ActionKinds=@() } }
1337
- if ($grant.schema -ne 'deadbyte.desktop-grant.v1') { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='grant-schema'; ActionKinds=@() } }
1338
- if ([string]$grant.policy_sha256 -ne [string]$meta.Sha256) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='policy-digest'; ActionKinds=@() } }
1339
- if ([string]$grant.instance_nonce -cnotmatch '^[0-9a-f]{64}$') { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='nonce-invalid'; ActionKinds=@() } }
1340
- $actions = @($grant.action_kinds)
1341
- $unique = @($actions | Sort-Object -Unique)
1342
- if ($actions.Count -lt 1 -or $actions.Count -ne $unique.Count) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='actions-invalid'; ActionKinds=@() } }
1343
- if (@($actions | Where-Object { $_ -notin $meta.ActionKinds }).Count -gt 0) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='actions-outside-policy'; ActionKinds=@() } }
1344
- $epoch = Get-AuthorityEpochStatus $Cfg 'desktop' $meta.GrantFile ([string]$grant.instance_nonce)
1345
- if (-not $epoch.Trusted) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason=[string]$epoch.Reason; ActionKinds=$actions } }
1346
- return [pscustomobject]@{ Configured=$true; Armed=$true; Reason='standing-grant'; ActionKinds=$actions; EpochSeq=$epoch.Seq; EpochSha256=$epoch.EventSha256 }
1347
- }
1348
-
1349
- function Arm-Desktop($Cfg) {
1350
- $meta = Get-DesktopPolicyMetadata $Cfg
1351
- Ensure-Directory $RuntimeDir
1352
- Ensure-Directory $meta.EvidenceDir
1353
- $nonceBytes = New-Object byte[] 32
1354
- $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
1355
- try { $rng.GetBytes($nonceBytes) } finally { $rng.Dispose() }
1356
- $nonce = -join ($nonceBytes | ForEach-Object { $_.ToString('x2') })
1357
- $body = [ordered]@{
1358
- schema = 'deadbyte.desktop-grant.v1'
1359
- policy_sha256 = [string]$meta.Sha256
1360
- instance_nonce = $nonce
1361
- action_kinds = @($meta.ActionKinds)
1362
- granted_at_utc = [DateTime]::UtcNow.ToString('o')
1363
- }
1364
- $tmp = $meta.GrantFile + '.tmp'
1365
- $body | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $tmp -Encoding UTF8
1366
- Move-Item -LiteralPath $tmp -Destination $meta.GrantFile -Force
1367
- try { Invoke-AuthorityEpochCommand $Cfg 'desktop' 'grant' $meta.GrantFile | Out-Null }
1368
- catch {
1369
- Remove-Item -LiteralPath $meta.GrantFile -Force -ErrorAction SilentlyContinue
1370
- throw
1371
- }
1372
- Write-Host ("DESKTOP PLANE ENABLED UNTIL EXPLICIT REVOKE [{0}]" -f ($meta.ActionKinds -join ','))
1373
- }
1374
-
1375
- function Disarm-Desktop($Cfg, [switch]$Quiet) {
1376
- $grantFile = Get-DesktopGrantFileForRevoke $Cfg
1377
- if ([string]::IsNullOrWhiteSpace([string]$grantFile)) { return }
1378
- $epochError = $null
1379
- if (Test-Path -LiteralPath $grantFile -PathType Leaf) {
1380
- try { Invoke-AuthorityEpochCommand $Cfg 'desktop' 'user-revoke' $grantFile | Out-Null } catch { $epochError = $_.Exception }
1381
- Remove-Item -LiteralPath $grantFile -Force -ErrorAction SilentlyContinue
1382
- }
1383
- if (Test-Path -LiteralPath $grantFile) { throw 'Desktop authority revocation failed: grant remains present' }
1384
- if ($null -ne $epochError) { throw $epochError }
1385
- if (-not $Quiet) { Write-Host 'DESKTOP PLANE DISARMED' }
1386
- }
1387
-
1388
- function Show-DesktopStatus($Cfg) {
1389
- $d = Get-DesktopGrantStatus $Cfg
1390
- Write-Host ("Desktop plane : {0} [{1}]" -f $(if ($d.Armed) { 'ARMED' } elseif ($d.Configured) { 'DISARMED' } else { 'UNCONFIGURED' }), $d.Reason)
1391
- return $d
1392
- }
1393
-
1394
- function Get-AutonomousSupervisorStatus($Cfg) {
1395
- if (-not [bool]$Cfg.AutonomousSupervisorEnabled) { return [pscustomobject]@{ Configured=$false; Running=$false; Reason='disabled'; Owned=$null } }
1396
- $owned = Test-OwnedInstance 'Supervisor'
1397
- [pscustomobject]@{ Configured=$true; Running=[bool]$owned.Alive; Reason=[string]$owned.Reason; Owned=$owned }
1398
- }
1399
-
1400
- function Start-AutonomousSupervisor($Cfg) {
1401
- if (-not [bool]$Cfg.AutonomousSupervisorEnabled) { Fail 'autonomous supervisor is disabled by trusted config' }
1402
- if (-not [bool]$Cfg.AutonomousRuntimeEnabled) { Fail 'autonomous supervisor requires AutonomousRuntimeEnabled' }
1403
- Start-OwnedComponent 'Supervisor' $Cfg.Supervisor $Cfg | Out-Null
1404
- $status = Get-AutonomousSupervisorStatus $Cfg
1405
- if (-not $status.Running) { Fail "autonomous supervisor start failed: $($status.Reason)" }
1406
- return $status
1407
- }
1408
-
1409
- function Stop-AutonomousSupervisor($Cfg) {
1410
- if (-not [bool]$Cfg.AutonomousSupervisorEnabled) { return Get-AutonomousSupervisorStatus $Cfg }
1411
- Stop-OwnedComponent 'Supervisor'
1412
- return Get-AutonomousSupervisorStatus $Cfg
1413
- }
1414
-
1415
- function Show-AutonomousSupervisorStatus($Cfg) {
1416
- $s = Get-AutonomousSupervisorStatus $Cfg
1417
- $label = if (-not $s.Configured) { 'DISABLED' } elseif ($s.Running) { 'RUNNING' } else { 'STOPPED' }
1418
- Write-Host ("Loop supervisor: {0} [{1}]" -f $label, $s.Reason)
1419
- return $s
1420
- }
1421
-
1422
- function Recover-AutonomousSupervisor($Cfg) {
1423
- if (-not [bool]$Cfg.AutonomousSupervisorEnabled) { Fail 'autonomous supervisor recovery requires AutonomousSupervisorEnabled' }
1424
- Disarm-Desktop $Cfg -Quiet
1425
- Start-Deadbyte $Cfg | Out-Null
1426
- try { Disarm-Autonomous $Cfg -Quiet } catch {}
1427
- try { Disarm-Coding $Cfg } catch {}
1428
- try { Disarm-Process $Cfg } catch {}
1429
- try { Disarm-Host $Cfg } catch {}
1430
- $runtime = Get-ObservedState $Cfg
1431
- if ($runtime.State -ne 'ONLINE') { Fail "supervisor recovery requires ONLINE runtime; observed $($runtime.State)" }
1432
- $process = Get-ProcessGrantStatus $Cfg
1433
- $coding = Get-CodingLeaseStatus $Cfg
1434
- $autonomous = Get-AutonomousLeaseStatus $Cfg
1435
- $desktop = Get-DesktopGrantStatus $Cfg
1436
- $hostStatus = Get-HostArmStatus $Cfg
1437
- if ($process.Armed -or $coding.Armed -or $autonomous.Armed -or $desktop.Armed -or $hostStatus.Armed) { Fail 'supervisor recovery final authority is not fully disarmed' }
1438
- $supervisor = Start-AutonomousSupervisor $Cfg
1439
- Write-Host 'AUTONOMOUS SUPERVISOR RECOVERY PASS: runtime ONLINE; privileged planes DISARMED.'
1440
- return [pscustomobject]@{ Runtime=$runtime; Supervisor=$supervisor; Process=$process; Coding=$coding; Autonomous=$autonomous; Desktop=$desktop; Host=$hostStatus }
1441
- }
1442
-
1443
- function Get-BoundedObservationReason($Value) {
1444
- $reason = [string]$Value
1445
- if ($reason -cmatch '^[a-z][a-z0-9-]{0,63}$') { return $reason }
1446
- return 'unverified'
1447
- }
1448
-
1449
- function Convert-OwnedObservation($Owned) {
1450
- $pidValue = $null; $birth = $null
1451
- if ($Owned.Alive -and $Owned.Ledger) {
1452
- $pidValue = [int]$Owned.Ledger.pid
1453
- $birth = [string]$Owned.Ledger.creation_time_utc
1454
- }
1455
- [ordered]@{ alive=[bool]$Owned.Alive; reason=(Get-BoundedObservationReason $Owned.Reason); pid=$pidValue; creation_time_utc=$birth }
1456
- }
1457
-
1458
- function Convert-AuthorityObservation($Authority) {
1459
- $out = [ordered]@{ configured=[bool]$Authority.Configured; armed=[bool]$Authority.Armed; reason=(Get-BoundedObservationReason $Authority.Reason) }
1460
- $seqProp = $Authority.PSObject.Properties['EpochSeq']
1461
- $shaProp = $Authority.PSObject.Properties['EpochSha256']
1462
- if ($null -ne $seqProp -or $null -ne $shaProp) {
1463
- if ($null -eq $seqProp -or $null -eq $shaProp -or [int64]$seqProp.Value -lt 1 -or [string]$shaProp.Value -cnotmatch '^[0-9a-f]{64}$') {
1464
- Fail 'authority epoch observation incomplete or invalid'
1465
- }
1466
- $out['epoch_seq'] = [int64]$seqProp.Value
1467
- $out['epoch_sha256'] = [string]$shaProp.Value
1468
- }
1469
- return $out
1470
- }
1471
-
1472
- function Get-DownstreamCoreObservation($Cfg, $Bridge) {
1473
- $out = [ordered]@{ alive=$false; reason='bridge-unavailable'; pid=$null; parent_pid=$null; creation_time_utc=$null; identity_basis=$null }
1474
- if (-not $Bridge.Alive) { return $out }
1475
- $bridgePid = [int]$Bridge.Ledger.pid
1476
- if (-not [string]::Equals([IO.Path]::GetFullPath([string]$Bridge.Ledger.script_path), [IO.Path]::GetFullPath([string]$Cfg.Bridge.ScriptPath), [StringComparison]::OrdinalIgnoreCase) -or [string]$Bridge.Ledger.script_sha256 -ne [string]$Cfg.Bridge.ScriptSha256) {
1477
- $out.reason='bridge-config-mismatch'; return $out
1478
- }
1479
- try {
1480
- $children = @(Get-CimInstance Win32_Process -Filter ("ParentProcessId={0} AND Name='node.exe'" -f $bridgePid) -ErrorAction Stop)
1481
- if ($children.Count -eq 0) { $out.reason='startup-zero-child'; return $out }
1482
- if ($children.Count -ne 1) { $out.reason='ambiguous-core'; return $out }
1483
- $child = $children[0]
1484
- if ([int]$child.ParentProcessId -ne $bridgePid -or [int]$child.ProcessId -lt 1 -or $null -eq $child.CreationDate) { $out.reason='core-identity-incomplete'; return $out }
1485
- if (-not [string]::IsNullOrWhiteSpace([string]$child.ExecutablePath) -and -not [string]::Equals([string]$child.ExecutablePath,[string]$Cfg.Bridge.FilePath,[StringComparison]::OrdinalIgnoreCase)) { $out.reason='core-image-mismatch'; return $out }
1486
- $command = [string]$child.CommandLine
1487
- $needle = (Join-Path ([string]$Cfg.CoreRoot) 'src\mcp-server.mjs').Replace('/','\').ToLowerInvariant()
1488
- if ([string]::IsNullOrWhiteSpace($command)) { $basis='verified-bridge-single-opaque-node-child' }
1489
- elseif ($command.Replace('/','\').ToLowerInvariant().Contains($needle)) { $basis='command-line' }
1490
- else { $out.reason='wrong-core-generation'; return $out }
1491
- $out.alive=$true; $out.reason='verified'; $out.pid=[int]$child.ProcessId; $out.parent_pid=$bridgePid
1492
- $out.creation_time_utc=$child.CreationDate.ToUniversalTime().ToString('o'); $out.identity_basis=$basis
1493
- return $out
1494
- } catch { $out.reason='core-observation-failed'; return $out }
1495
- }
1496
-
1497
- function Get-StructuredObservation($Cfg) {
1498
- $s = Get-ObservedState $Cfg
1499
- $supervisor = Get-AutonomousSupervisorStatus $Cfg
1500
- [ordered]@{
1501
- schema='deadbyte.controller-observation.v1'
1502
- observed_at_utc=[DateTime]::UtcNow.ToString('o')
1503
- state=[string]$s.State
1504
- core_root=[string]$Cfg.CoreRoot
1505
- local_ready=[bool]$s.LocalReady
1506
- bridge=(Convert-OwnedObservation $s.Bridge)
1507
- core=$s.Core
1508
- cloud=(Convert-OwnedObservation $s.Cloud)
1509
- supervisor=[ordered]@{ configured=[bool]$supervisor.Configured; running=[bool]$supervisor.Running; reason=(Get-BoundedObservationReason $supervisor.Reason) }
1510
- authority=[ordered]@{
1511
- process=(Convert-AuthorityObservation (Get-ProcessGrantStatus $Cfg))
1512
- coding=(Convert-AuthorityObservation (Get-CodingLeaseStatus $Cfg))
1513
- autonomous=(Convert-AuthorityObservation (Get-AutonomousLeaseStatus $Cfg))
1514
- desktop=(Convert-AuthorityObservation (Get-DesktopGrantStatus $Cfg))
1515
- host=(Convert-AuthorityObservation (Get-HostArmStatus $Cfg))
1516
- }
1517
- }
1518
- }
1519
-
1520
- function Show-State($Cfg) {
1521
- $s = Get-ObservedState $Cfg
1522
- $bp = if ($s.Bridge.Alive) { $s.Bridge.Ledger.pid } else { '-' }
1523
- $cp = if ($s.Cloud.Alive) { $s.Cloud.Ledger.pid } else { '-' }
1524
- $url = Get-CurrentUrl $Cfg
1525
- Write-Host ("DEADBYTE MCP = {0}" -f $s.State)
1526
- Write-Host ("Bridge PID : {0} [{1}]" -f $bp, $s.Bridge.Reason)
1527
- Write-Host ("Core PID : {0} [{1}]" -f $(if ($s.Core.alive) { $s.Core.pid } else { '-' }), $s.Core.reason)
1528
- Write-Host ("Cloud PID : {0} [{1}]" -f $cp, $s.Cloud.Reason)
1529
- Write-Host ("Local ready : {0}" -f $s.LocalReady)
1530
- Write-Host ("URL : {0}" -f $(if ($url) { $url } else { '<unknown>' }))
1531
- Write-Host ("Machine runtime: {0}" -f $(if ([bool]$Cfg.MachineRuntimeEnabled) { 'ENABLED' } else { 'DISABLED' }))
1532
- Write-Host ("Agent runtime : {0}" -f $(if ([bool]$Cfg.AgentRuntimeEnabled) { 'ENABLED' } else { 'DISABLED' }))
1533
- Write-Host ("Process rt : {0}" -f $(if ([bool]$Cfg.ProcessRuntimeEnabled) { 'ENABLED' } else { 'DISABLED' }))
1534
- Write-Host ("Coding runtime: {0}" -f $(if ([bool]$Cfg.CodingRuntimeEnabled) { 'ENABLED' } else { 'DISABLED' }))
1535
- Write-Host ("Machine FS rt : {0}" -f $(if ([bool]$Cfg.MachineFsRuntimeEnabled) { 'ENABLED' } else { 'DISABLED' }))
1536
- Write-Host ("Observation rt: {0}" -f $(if ([bool]$Cfg.ObservationRuntimeEnabled) { 'ENABLED' } else { 'DISABLED' }))
1537
- Write-Host ("Desktop runtime: {0}" -f $(if ([bool]$Cfg.DesktopRuntimeEnabled) { 'ENABLED' } else { 'DISABLED' }))
1538
- Write-Host ("Autonomous rt : {0}" -f $(if ([bool]$Cfg.AutonomousRuntimeEnabled) { 'ENABLED' } else { 'DISABLED' }))
1539
- Show-AutonomousSupervisorStatus $Cfg | Out-Null
1540
- Show-ProcessStatus $Cfg | Out-Null
1541
- Show-CodingStatus $Cfg | Out-Null
1542
- Show-AutonomousStatus $Cfg | Out-Null
1543
- Show-DesktopStatus $Cfg | Out-Null
1544
- Show-HostStatus $Cfg | Out-Null
1545
- return $s
1546
- }
1547
-
1548
- function Invoke-MachineSmoke($Cfg) {
1549
- if (-not [bool]$Cfg.MachineRuntimeEnabled) { Fail 'machinetest requires MachineRuntimeEnabled' }
1550
- $s = Get-ObservedState $Cfg
1551
- if ($s.State -ne 'ONLINE') { Fail "machinetest requires ONLINE; observed $($s.State)" }
1552
- $url = Get-CurrentUrl $Cfg
1553
- if (-not $url) { Fail 'public MCP URL unavailable' }
1554
- Wait-PublicDnsReady $Cfg $url
1555
- $probeArgs = @($url)
1556
- if (-not [string]::IsNullOrWhiteSpace([string]$env:DEADBYTE_REMOTE_PROBE_MODE)) {
1557
- if ([string]$env:DEADBYTE_REMOTE_PROBE_MODE -notin @('default','desktop','host','full','revoked')) { Fail 'invalid remote enforcement probe mode' }
1558
- $probeArgs += [string]$env:DEADBYTE_REMOTE_PROBE_MODE
1559
- }
1560
- Invoke-CoreGate $Cfg 'remote-mcp-machine-smoke-client.mjs' $probeArgs
1561
- Write-Host 'MACHINE IDENTITY LIVE GATE PASS: public status/list + SPKI identity + signed Ed25519 ping verified.'
1562
- }
1563
-
1564
- function Invoke-Smoke($Cfg, [bool]$Full) {
1565
- $s = Get-ObservedState $Cfg
1566
- if ($s.State -ne 'ONLINE') { Fail "MCP test requires ONLINE; observed $($s.State)" }
1567
- if (-not (Test-LocalReady $Cfg)) { Fail 'local readiness probe failed' }
1568
- $url = Get-CurrentUrl $Cfg
1569
- if (-not $url) { Fail 'public MCP URL unavailable; configure McpUrlFile or McpPathFile' }
1570
- Wait-PublicDnsReady $Cfg $url
1571
- Invoke-CoreGate $Cfg 'remote-mcp-smoke-client.mjs' @($url, $(if ($Full) { 'full' } else { 'smoke' }))
1572
- if ($Full) {
1573
- Invoke-CoreGate $Cfg 'mcp-contained-smoke-client.mjs'
1574
- Write-Host 'FULL GATE PASS: public modern MCP + proof + public contained proof + local canonical 3-operation AppContainer/replay/evidence gate.'
1575
- } else {
1576
- Write-Host 'MCP SMOKE PASS: public modern MCP + exact tool catalog + signed proof + independent verification.'
1577
- }
1578
- }
1579
-
1580
- function Invoke-HostSmoke($Cfg) {
1581
- $h = Get-HostArmStatus $Cfg
1582
- if (-not $h.Armed) { Fail 'hosttest requires an active local arm lease; run armhost first' }
1583
- $s = Get-ObservedState $Cfg
1584
- if ($s.State -ne 'ONLINE') { Fail "hosttest requires ONLINE; observed $($s.State)" }
1585
- $url = Get-CurrentUrl $Cfg
1586
- if (-not $url) { Fail 'public MCP URL unavailable' }
1587
- Wait-PublicDnsReady $Cfg $url
1588
- Invoke-CoreGate $Cfg 'remote-mcp-host-smoke-client.mjs' @($url)
1589
- Write-Host 'HOST PLANE LIVE GATE PASS: public host write + hash readback + hash-pinned exec + cleanup.'
1590
- }
1591
-
1592
- function Invoke-CodingSmoke($Cfg) {
1593
- $c = Get-CodingLeaseStatus $Cfg
1594
- if (-not $c.Armed) { Fail 'codingtest requires an active local coding lease; run armcoding first' }
1595
- $s = Get-ObservedState $Cfg
1596
- if ($s.State -ne 'ONLINE') { Fail "codingtest requires ONLINE; observed $($s.State)" }
1597
- $url = Get-CurrentUrl $Cfg
1598
- if (-not $url) { Fail 'public MCP URL unavailable' }
1599
- Wait-PublicDnsReady $Cfg $url
1600
- Invoke-CoreGate $Cfg 'remote-mcp-coding-smoke-client.mjs' @($url)
1601
- Write-Host 'CODING PLANE LIVE GATE PASS: public edit + command + benchmark + durable coding-task receipt verification.'
1602
- }
1603
-
1604
- function Invoke-AutonomousSmoke($Cfg) {
1605
- $a = Get-AutonomousLeaseStatus $Cfg
1606
- if (-not $a.Armed) { Fail 'autonomoustest requires active coding + autonomous leases; run armcoding then armautonomous' }
1607
- $s = Get-ObservedState $Cfg
1608
- if ($s.State -ne 'ONLINE') { Fail "autonomoustest requires ONLINE; observed $($s.State)" }
1609
- $url = Get-CurrentUrl $Cfg
1610
- if (-not $url) { Fail 'public MCP URL unavailable' }
1611
- Wait-PublicDnsReady $Cfg $url
1612
- Invoke-CoreGate $Cfg 'remote-mcp-autonomous-smoke-client.mjs' @($url)
1613
- Write-Host 'AUTONOMOUS PRODUCTION LIVE GATE PASS: public sampling MRTR + native diff + fail/diagnose/replan + build/test/benchmark + signed release gate.'
1614
- }
1615
-
1616
- function Clean-Stale {
1617
- Ensure-Directory $RuntimeDir
1618
- foreach ($name in @('Bridge','Cloud','Supervisor')) {
1619
- $o = Test-OwnedInstance $name
1620
- if ($o.Ledger -and -not $o.Alive -and $o.Reason -eq 'process-missing') {
1621
- Remove-Item -LiteralPath (Ledger-Path $name) -Force -ErrorAction SilentlyContinue
1622
- } elseif ($o.Ledger -and -not $o.Alive -and $o.Reason -ne 'no-ledger') {
1623
- Write-Warning "$name ledger retained: $($o.Reason). No unverified process was killed."
1624
- }
1625
- }
1626
- }
1627
-
1628
- function Run-CommandCore([string]$Name) {
1629
- $cfg = Get-Config
1630
- Assert-ControllerRootBound $cfg
1631
- $readOnly = $Name -in @('status','url','hoststatus','processstatus','codingstatus','autonomousstatus','desktopstatus','supervisorstatus')
1632
- if (-not $readOnly) { Ensure-Directory $RuntimeDir; Ensure-Directory $LogDir }
1633
- switch ($Name) {
1634
- 'start' { Start-Deadbyte $cfg | Out-Null; if ([bool]$cfg.AutonomousSupervisorEnabled) { Start-AutonomousSupervisor $cfg | Out-Null }; Show-State $cfg | Out-Null }
1635
- 'resume' { Start-Deadbyte $cfg | Out-Null; if ([bool]$cfg.AutonomousSupervisorEnabled) { Start-AutonomousSupervisor $cfg | Out-Null }; Show-State $cfg | Out-Null }
1636
- 'park' { Park-Deadbyte $cfg | Out-Null; Show-State $cfg | Out-Null }
1637
- 'stop' { if ([bool]$cfg.AutonomousSupervisorEnabled) { Stop-AutonomousSupervisor $cfg | Out-Null }; Stop-Deadbyte $cfg | Out-Null; Show-State $cfg | Out-Null }
1638
- 'restart' { if ([bool]$cfg.AutonomousSupervisorEnabled) { Stop-AutonomousSupervisor $cfg | Out-Null }; Stop-Deadbyte $cfg | Out-Null; Start-Deadbyte $cfg | Out-Null; if ([bool]$cfg.AutonomousSupervisorEnabled) { Start-AutonomousSupervisor $cfg | Out-Null }; Show-State $cfg | Out-Null }
1639
- 'status' { if ($Json) { Get-StructuredObservation $cfg | ConvertTo-Json -Depth 6 -Compress | Write-Output } else { Show-State $cfg | Out-Null } }
1640
- 'url' { $u=Get-CurrentUrl $cfg; if (-not $u) { Fail 'URL unavailable' }; Write-Output $u }
1641
- 'test' { Invoke-Smoke $cfg $false }
1642
- 'fulltest'{ Invoke-Smoke $cfg $true }
1643
- 'toggle' { $s=Get-ObservedState $cfg; if ($s.State -eq 'ONLINE') { Park-Deadbyte $cfg | Out-Null } elseif ($s.State -in @('PARKED','OFFLINE')) { Start-Deadbyte $cfg | Out-Null } else { Fail 'toggle refuses DEGRADED state' }; Show-State $cfg | Out-Null }
1644
- 'clean' { Clean-Stale; Show-State $cfg | Out-Null }
1645
- 'machinetest' { Invoke-MachineSmoke $cfg }
1646
- 'armhost' { Arm-Host $cfg; Show-HostStatus $cfg | Out-Null }
1647
- 'disarmhost' { Disarm-Host $cfg; Show-HostStatus $cfg | Out-Null }
1648
- 'hoststatus' { Show-HostStatus $cfg | Out-Null }
1649
- 'hosttest' { Invoke-HostSmoke $cfg }
1650
- 'armprocess' { Arm-Process $cfg; Show-ProcessStatus $cfg | Out-Null }
1651
- 'disarmprocess' { Disarm-Process $cfg; Show-ProcessStatus $cfg | Out-Null }
1652
- 'processstatus' { Show-ProcessStatus $cfg | Out-Null }
1653
- 'armcoding' { Arm-Coding $cfg; Show-CodingStatus $cfg | Out-Null }
1654
- 'disarmcoding' { Disarm-Coding $cfg; Show-CodingStatus $cfg | Out-Null; Show-AutonomousStatus $cfg | Out-Null }
1655
- 'codingstatus' { Show-CodingStatus $cfg | Out-Null }
1656
- 'codingtest' { Invoke-CodingSmoke $cfg }
1657
- 'armautonomous' { Arm-Autonomous $cfg; Show-AutonomousStatus $cfg | Out-Null }
1658
- 'disarmautonomous' { Disarm-Autonomous $cfg; Show-AutonomousStatus $cfg | Out-Null }
1659
- 'autonomousstatus' { Show-AutonomousStatus $cfg | Out-Null }
1660
- 'autonomoustest' { Invoke-AutonomousSmoke $cfg }
1661
- 'armdesktop' { Arm-Desktop $cfg; Show-DesktopStatus $cfg | Out-Null }
1662
- 'disarmdesktop' { Disarm-Desktop $cfg; Show-DesktopStatus $cfg | Out-Null }
1663
- 'desktopstatus' { Show-DesktopStatus $cfg | Out-Null }
1664
- 'supervisorstart' { Start-AutonomousSupervisor $cfg | Out-Null; Show-AutonomousSupervisorStatus $cfg | Out-Null }
1665
- 'supervisorstop' { Stop-AutonomousSupervisor $cfg | Out-Null; Show-AutonomousSupervisorStatus $cfg | Out-Null }
1666
- 'supervisorstatus' { Show-AutonomousSupervisorStatus $cfg | Out-Null }
1667
- 'supervisorrecover' { Recover-AutonomousSupervisor $cfg | Out-Null; Show-State $cfg | Out-Null }
1668
- default { Fail "unknown command '$Name'" }
1669
- }
1670
- }
1671
-
1672
- function Run-Command([string]$Name) {
1673
- $mutating = $Name -in @(
1674
- 'start','resume','park','stop','restart','toggle','clean',
1675
- 'armhost','disarmhost','armprocess','disarmprocess','armcoding','disarmcoding',
1676
- 'armautonomous','disarmautonomous','armdesktop','disarmdesktop',
1677
- 'supervisorstart','supervisorstop','supervisorrecover'
1678
- )
1679
- if (-not $mutating) {
1680
- Run-CommandCore $Name
1681
- return
1682
- }
1683
- Invoke-WithControllerMutationLock $Name { Run-CommandCore $Name }
1684
- }
1685
-
1686
- if ($Command -ne 'menu') {
1687
- $recordsStartFailure = $Command -in @('start','resume','restart')
1688
- if ($recordsStartFailure) { Remove-Item -LiteralPath (Controller-StartFailurePath) -Force -ErrorAction SilentlyContinue }
1689
- try {
1690
- Run-Command $Command
1691
- exit 0
1692
- } catch {
1693
- if ($recordsStartFailure) {
1694
- try { Write-ControllerStartFailure $Command $_.Exception } catch {}
1695
- }
1696
- throw
1697
- }
1698
- }
1699
-
1700
- while ($true) {
1701
- Clear-Host
1702
- $cfg = Get-Config
1703
- Assert-ControllerRootBound $cfg
1704
- Write-Host 'DEADBYTE MCP ON-DEMAND CONTROLLER V0.15.5 / R46'
1705
- Write-Host '======================================'
1706
- Show-State $cfg | Out-Null
1707
- Write-Host ''
1708
- Write-Host '[1] Start / Resume'
1709
- Write-Host '[2] Park'
1710
- Write-Host '[3] Full Stop'
1711
- Write-Host '[4] Restart'
1712
- Write-Host '[5] Transport smoke test'
1713
- Write-Host '[6] Public transport gate'
1714
- Write-Host '[7] Show current URL'
1715
- Write-Host '[8] Clean stale ledgers'
1716
- Write-Host '[9] Enable privileged host plane (standing grant)'
1717
- Write-Host '[10] Revoke privileged host plane'
1718
- Write-Host '[11] Host plane status'
1719
- Write-Host '[12] Host plane LIVE gate (requires grant)'
1720
- Write-Host '[13] Enable coding plane (standing grant)'
1721
- Write-Host '[14] Revoke coding plane'
1722
- Write-Host '[15] Coding plane status'
1723
- Write-Host '[16] Coding plane LIVE gate (requires grant)'
1724
- Write-Host '[17] Enable autonomous plane (standing grant)'
1725
- Write-Host '[18] Revoke autonomous plane'
1726
- Write-Host '[19] Autonomous plane status'
1727
- Write-Host '[20] Autonomous production LIVE gate'
1728
- Write-Host '[21] Machine identity LIVE gate (non-mutating)'
1729
- Write-Host '[22] Enable process plane (standing grant)'
1730
- Write-Host '[23] Revoke process plane'
1731
- Write-Host '[24] Process plane status'
1732
- Write-Host '[25] Enable desktop plane (standing grant; never auto-armed)'
1733
- Write-Host '[26] Revoke desktop plane'
1734
- Write-Host '[27] Desktop plane status'
1735
- Write-Host '[0] Exit controller (runtime stays as-is)'
1736
- $choice = Read-Host 'Select'
1737
- try {
1738
- switch ($choice) {
1739
- '1' { Run-Command 'start' }
1740
- '2' { Run-Command 'park' }
1741
- '3' { Run-Command 'stop' }
1742
- '4' { Run-Command 'restart' }
1743
- '5' { Run-Command 'test' }
1744
- '6' { Run-Command 'fulltest' }
1745
- '7' { Run-Command 'url' }
1746
- '8' { Run-Command 'clean' }
1747
- '9' { Run-Command 'armhost' }
1748
- '10' { Run-Command 'disarmhost' }
1749
- '11' { Run-Command 'hoststatus' }
1750
- '12' { Run-Command 'hosttest' }
1751
- '13' { Run-Command 'armcoding' }
1752
- '14' { Run-Command 'disarmcoding' }
1753
- '15' { Run-Command 'codingstatus' }
1754
- '16' { Run-Command 'codingtest' }
1755
- '17' { Run-Command 'armautonomous' }
1756
- '18' { Run-Command 'disarmautonomous' }
1757
- '19' { Run-Command 'autonomousstatus' }
1758
- '20' { Run-Command 'autonomoustest' }
1759
- '21' { Run-Command 'machinetest' }
1760
- '22' { Run-Command 'armprocess' }
1761
- '23' { Run-Command 'disarmprocess' }
1762
- '24' { Run-Command 'processstatus' }
1763
- '25' { Run-Command 'armdesktop' }
1764
- '26' { Run-Command 'disarmdesktop' }
1765
- '27' { Run-Command 'desktopstatus' }
1766
- '0' { exit 0 }
1767
- default { Write-Warning 'invalid selection' }
1768
- }
1769
- } catch { Write-Host $_.Exception.Message -ForegroundColor Red }
1770
- if ($choice -ne '0') { [void](Read-Host 'Press Enter') }
1771
- }
556
+ $env:DEADBYTE_PROCESS_RUNTIME = if ([bool]$Cfg.ProcessRuntimeEnabled) { '1' } else { '0' }
557
+ if (-not [string]::IsNullOrWhiteSpace([string]$Cfg.ProcessPolicyFile)) {
558
+ $env:DEADBYTE_PROCESS_POLICY = [string]$Cfg.ProcessPolicyFile
559
+ }
560
+ $env:DEADBYTE_CODING_RUNTIME = if ([bool]$Cfg.CodingRuntimeEnabled) { '1' } else { '0' }
561
+ $env:DEADBYTE_MACHINE_FS_RUNTIME = if ([bool]$Cfg.MachineFsRuntimeEnabled) { '1' } else { '0' }
562
+ $env:DEADBYTE_OBSERVATION_RUNTIME = if ([bool]$Cfg.ObservationRuntimeEnabled) { '1' } else { '0' }
563
+ $env:DEADBYTE_DESKTOP_RUNTIME = if ([bool]$Cfg.DesktopRuntimeEnabled) { '1' } else { '0' }
564
+ if (-not [string]::IsNullOrWhiteSpace([string]$Cfg.DesktopPolicyFile)) {
565
+ $env:DEADBYTE_DESKTOP_POLICY = [string]$Cfg.DesktopPolicyFile
566
+ } else {
567
+ Remove-Item Env:DEADBYTE_DESKTOP_POLICY -ErrorAction SilentlyContinue
568
+ }
569
+ if (-not [string]::IsNullOrWhiteSpace([string]$Cfg.CodingPolicyFile)) {
570
+ $env:DEADBYTE_CODING_POLICY = [string]$Cfg.CodingPolicyFile
571
+ }
572
+ if (-not [string]::IsNullOrWhiteSpace([string]$Cfg.SemanticLspConfigFile)) {
573
+ $env:DEADBYTE_SEMANTIC_LSP_CONFIG = [string]$Cfg.SemanticLspConfigFile
574
+ $env:DEADBYTE_SEMANTIC_LSP_CONFIG_SHA256 = [string]$Cfg.SemanticLspConfigSha256
575
+ } else {
576
+ Remove-Item Env:DEADBYTE_SEMANTIC_LSP_CONFIG -ErrorAction SilentlyContinue
577
+ Remove-Item Env:DEADBYTE_SEMANTIC_LSP_CONFIG_SHA256 -ErrorAction SilentlyContinue
578
+ }
579
+ $env:DEADBYTE_AUTONOMOUS_RUNTIME = if ([bool]$Cfg.AutonomousRuntimeEnabled) { '1' } else { '0' }
580
+ $env:DEADBYTE_AUTONOMOUS_SUPERVISOR_MODE = if ([bool]$Cfg.AutonomousSupervisorEnabled) { 'external' } else { 'in_process' }
581
+ if (-not [string]::IsNullOrWhiteSpace([string]$Cfg.AutonomousPolicyFile)) {
582
+ $env:DEADBYTE_AUTONOMOUS_POLICY = [string]$Cfg.AutonomousPolicyFile
583
+ }
584
+ if (-not [string]::IsNullOrWhiteSpace([string]$Cfg.HostPolicyFile)) {
585
+ $env:DEADBYTE_PRIVILEGED_POLICY = [string]$Cfg.HostPolicyFile
586
+ }
587
+ } elseif ($Name -eq 'Supervisor' -and $Cfg) {
588
+ Remove-Item Env:DEADBYTE_CAPABILITY_PROFILE -ErrorAction SilentlyContinue
589
+ Remove-Item Env:DEADBYTE_AUDIT_RUNTIME -ErrorAction SilentlyContinue
590
+ Remove-Item Env:DEADBYTE_PRIVILEGED_POLICY -ErrorAction SilentlyContinue
591
+ Remove-Item Env:DEADBYTE_MACHINE_RUNTIME -ErrorAction SilentlyContinue
592
+ Remove-Item Env:DEADBYTE_AGENT_RUNTIME -ErrorAction SilentlyContinue
593
+ Remove-Item Env:DEADBYTE_PROCESS_RUNTIME -ErrorAction SilentlyContinue
594
+ Remove-Item Env:DEADBYTE_PROCESS_POLICY -ErrorAction SilentlyContinue
595
+ Remove-Item Env:DEADBYTE_MACHINE_FS_RUNTIME -ErrorAction SilentlyContinue
596
+ Remove-Item Env:DEADBYTE_OBSERVATION_RUNTIME -ErrorAction SilentlyContinue
597
+ Remove-Item Env:DEADBYTE_DESKTOP_RUNTIME -ErrorAction SilentlyContinue
598
+ Remove-Item Env:DEADBYTE_DESKTOP_POLICY -ErrorAction SilentlyContinue
599
+ Remove-Item Env:DEADBYTE_SEMANTIC_LSP_CONFIG -ErrorAction SilentlyContinue
600
+ Remove-Item Env:DEADBYTE_SEMANTIC_LSP_CONFIG_SHA256 -ErrorAction SilentlyContinue
601
+ Remove-Item Env:DEADBYTE_AUTONOMOUS_SUPERVISOR_MODE -ErrorAction SilentlyContinue
602
+ $env:DEADBYTE_CODING_RUNTIME = '1'
603
+ $env:DEADBYTE_AUTONOMOUS_RUNTIME = '1'
604
+ $env:DEADBYTE_CODING_POLICY = [string]$Cfg.CodingPolicyFile
605
+ $env:DEADBYTE_AUTONOMOUS_POLICY = [string]$Cfg.AutonomousPolicyFile
606
+ $env:DEADBYTE_SUPERVISOR_ONCE = '0'
607
+ $env:DEADBYTE_SUPERVISOR_MARKER = Join-Path $RuntimeDir 'autonomous-supervisor-owner.json'
608
+ } else {
609
+ Remove-Item Env:DEADBYTE_CAPABILITY_PROFILE -ErrorAction SilentlyContinue
610
+ Remove-Item Env:DEADBYTE_AUDIT_RUNTIME -ErrorAction SilentlyContinue
611
+ }
612
+ $proc = Start-Process @start
613
+ } finally {
614
+ if ($hadCoreRootEnv) { $env:DEADBYTE_CORE_ROOT = $previousCoreRootEnv }
615
+ else { Remove-Item Env:DEADBYTE_CORE_ROOT -ErrorAction SilentlyContinue }
616
+ if ($hadHostPolicyEnv) { $env:DEADBYTE_PRIVILEGED_POLICY = $previousHostPolicyEnv }
617
+ else { Remove-Item Env:DEADBYTE_PRIVILEGED_POLICY -ErrorAction SilentlyContinue }
618
+ if ($hadWorkspaceRootEnv) { $env:DEADBYTE_WORKSPACE_ROOT = $previousWorkspaceRootEnv }
619
+ else { Remove-Item Env:DEADBYTE_WORKSPACE_ROOT -ErrorAction SilentlyContinue }
620
+ if ($hadAuditRuntimeEnv) { $env:DEADBYTE_AUDIT_RUNTIME = $previousAuditRuntimeEnv }
621
+ else { Remove-Item Env:DEADBYTE_AUDIT_RUNTIME -ErrorAction SilentlyContinue }
622
+ if ($hadMachineRuntimeEnv) { $env:DEADBYTE_MACHINE_RUNTIME = $previousMachineRuntimeEnv }
623
+ else { Remove-Item Env:DEADBYTE_MACHINE_RUNTIME -ErrorAction SilentlyContinue }
624
+ if ($hadAgentRuntimeEnv) { $env:DEADBYTE_AGENT_RUNTIME = $previousAgentRuntimeEnv }
625
+ else { Remove-Item Env:DEADBYTE_AGENT_RUNTIME -ErrorAction SilentlyContinue }
626
+ if ($hadProcessRuntimeEnv) { $env:DEADBYTE_PROCESS_RUNTIME = $previousProcessRuntimeEnv }
627
+ else { Remove-Item Env:DEADBYTE_PROCESS_RUNTIME -ErrorAction SilentlyContinue }
628
+ if ($hadProcessPolicyEnv) { $env:DEADBYTE_PROCESS_POLICY = $previousProcessPolicyEnv }
629
+ else { Remove-Item Env:DEADBYTE_PROCESS_POLICY -ErrorAction SilentlyContinue }
630
+ if ($hadCodingRuntimeEnv) { $env:DEADBYTE_CODING_RUNTIME = $previousCodingRuntimeEnv }
631
+ else { Remove-Item Env:DEADBYTE_CODING_RUNTIME -ErrorAction SilentlyContinue }
632
+ if ($hadMachineFsRuntimeEnv) { $env:DEADBYTE_MACHINE_FS_RUNTIME = $previousMachineFsRuntimeEnv }
633
+ else { Remove-Item Env:DEADBYTE_MACHINE_FS_RUNTIME -ErrorAction SilentlyContinue }
634
+ if ($hadObservationRuntimeEnv) { $env:DEADBYTE_OBSERVATION_RUNTIME = $previousObservationRuntimeEnv }
635
+ else { Remove-Item Env:DEADBYTE_OBSERVATION_RUNTIME -ErrorAction SilentlyContinue }
636
+ if ($hadDesktopRuntimeEnv) { $env:DEADBYTE_DESKTOP_RUNTIME = $previousDesktopRuntimeEnv }
637
+ else { Remove-Item Env:DEADBYTE_DESKTOP_RUNTIME -ErrorAction SilentlyContinue }
638
+ if ($hadDesktopPolicyEnv) { $env:DEADBYTE_DESKTOP_POLICY = $previousDesktopPolicyEnv }
639
+ else { Remove-Item Env:DEADBYTE_DESKTOP_POLICY -ErrorAction SilentlyContinue }
640
+ if ($hadCodingPolicyEnv) { $env:DEADBYTE_CODING_POLICY = $previousCodingPolicyEnv }
641
+ else { Remove-Item Env:DEADBYTE_CODING_POLICY -ErrorAction SilentlyContinue }
642
+ if ($hadSemanticConfigEnv) { $env:DEADBYTE_SEMANTIC_LSP_CONFIG = $previousSemanticLspConfigEnv }
643
+ else { Remove-Item Env:DEADBYTE_SEMANTIC_LSP_CONFIG -ErrorAction SilentlyContinue }
644
+ if ($hadSemanticConfigShaEnv) { $env:DEADBYTE_SEMANTIC_LSP_CONFIG_SHA256 = $previousSemanticLspConfigShaEnv }
645
+ else { Remove-Item Env:DEADBYTE_SEMANTIC_LSP_CONFIG_SHA256 -ErrorAction SilentlyContinue }
646
+ if ($hadAutonomousRuntimeEnv) { $env:DEADBYTE_AUTONOMOUS_RUNTIME = $previousAutonomousRuntimeEnv }
647
+ else { Remove-Item Env:DEADBYTE_AUTONOMOUS_RUNTIME -ErrorAction SilentlyContinue }
648
+ if ($hadAutonomousPolicyEnv) { $env:DEADBYTE_AUTONOMOUS_POLICY = $previousAutonomousPolicyEnv }
649
+ else { Remove-Item Env:DEADBYTE_AUTONOMOUS_POLICY -ErrorAction SilentlyContinue }
650
+ if ($hadSupervisorOnceEnv) { $env:DEADBYTE_SUPERVISOR_ONCE = $previousSupervisorOnceEnv }
651
+ else { Remove-Item Env:DEADBYTE_SUPERVISOR_ONCE -ErrorAction SilentlyContinue }
652
+ if ($hadSupervisorMarkerEnv) { $env:DEADBYTE_SUPERVISOR_MARKER = $previousSupervisorMarkerEnv }
653
+ else { Remove-Item Env:DEADBYTE_SUPERVISOR_MARKER -ErrorAction SilentlyContinue }
654
+ if ($hadAutonomousSupervisorModeEnv) { $env:DEADBYTE_AUTONOMOUS_SUPERVISOR_MODE = $previousAutonomousSupervisorModeEnv }
655
+ else { Remove-Item Env:DEADBYTE_AUTONOMOUS_SUPERVISOR_MODE -ErrorAction SilentlyContinue }
656
+ if ($hadCapabilityProfileEnv) { $env:DEADBYTE_CAPABILITY_PROFILE = $previousCapabilityProfileEnv }
657
+ else { Remove-Item Env:DEADBYTE_CAPABILITY_PROFILE -ErrorAction SilentlyContinue }
658
+ }
659
+ Start-Sleep -Milliseconds 150
660
+ $obs = Get-ProcessObservation ([int]$proc.Id)
661
+ if (-not $obs) { Fail "$Name exited before identity capture; see $stderr" }
662
+ $expected = [System.IO.Path]::GetFullPath([string]$Spec.FilePath)
663
+ if (-not [string]::Equals($obs.image_path, $expected, [System.StringComparison]::OrdinalIgnoreCase)) {
664
+ try { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue } catch {}
665
+ Fail "$Name spawned a wrapper instead of the configured long-lived executable; observed '$($obs.image_path)'"
666
+ }
667
+ if ($obs.image_sha256 -ne (Get-ExecutableHash $expected)) {
668
+ try { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue } catch {}
669
+ Fail "$Name executable changed during start"
670
+ }
671
+ Write-Ledger $Name $obs $nonce $Spec
672
+ return Test-OwnedInstance $Name
673
+ }
674
+
675
+ function Stop-OwnedComponent([string]$Name) {
676
+ $owned = Test-OwnedInstance $Name
677
+ if (-not $owned.Ledger) { return }
678
+ if (-not $owned.Alive) {
679
+ if ($owned.Reason -eq 'process-missing') { Remove-Item -LiteralPath (Ledger-Path $Name) -Force -ErrorAction SilentlyContinue; return }
680
+ if ($owned.Reason -eq 'pid-reused') { Reconcile-PidReusedLedger $Name $owned | Out-Null; return }
681
+ Fail "$Name is not provably owned ($($owned.Reason)); refusing Stop-Process"
682
+ }
683
+ Stop-Process -Id ([int]$owned.Ledger.pid) -Force -ErrorAction Stop
684
+ try { Wait-Process -Id ([int]$owned.Ledger.pid) -Timeout 5 -ErrorAction SilentlyContinue } catch {}
685
+ Remove-Item -LiteralPath (Ledger-Path $Name) -Force -ErrorAction SilentlyContinue
686
+ }
687
+
688
+ function Get-LocalHealth($Cfg) {
689
+ $response = $null
690
+ try {
691
+ $request = [System.Net.HttpWebRequest]::Create([string]$Cfg.LocalReadyUri)
692
+ $request.Timeout = [int]$Cfg.ProbeTimeoutSeconds * 1000
693
+ $request.ReadWriteTimeout = [int]$Cfg.ProbeTimeoutSeconds * 1000
694
+ $response = $request.GetResponse()
695
+ } catch [System.Net.WebException] {
696
+ $response = $_.Exception.Response
697
+ if ($null -eq $response) { return [pscustomobject]@{ State='UNREACHABLE'; StatusCode=0; Diagnostic='readiness endpoint unreachable' } }
698
+ } catch { return [pscustomobject]@{ State='UNREACHABLE'; StatusCode=0; Diagnostic='readiness probe failed' } }
699
+ try {
700
+ $status = [int]$response.StatusCode
701
+ $reader = New-Object System.IO.StreamReader($response.GetResponseStream())
702
+ try { $body = $reader.ReadToEnd() } finally { $reader.Dispose() }
703
+ try { $parsed = $body | ConvertFrom-Json } catch { return [pscustomobject]@{ State='INVALID'; StatusCode=$status; Diagnostic='readiness response JSON invalid' } }
704
+ if ($parsed.schema -ne 'deadbyte.bridge-health.v1' -or [string]$parsed.state -notin @('STARTING','READY','DEGRADED','RECONCILING','STOPPING')) {
705
+ return [pscustomobject]@{ State='INVALID'; StatusCode=$status; Diagnostic='readiness response schema invalid' }
706
+ }
707
+ return [pscustomobject]@{ State=[string]$parsed.state; StatusCode=$status; Diagnostic=[string]$parsed.diagnostic }
708
+ } finally { if ($null -ne $response) { $response.Dispose() } }
709
+ }
710
+ function Test-LocalReady($Cfg) {
711
+ $health = Get-LocalHealth $Cfg
712
+ return ($health.State -eq 'READY' -and $health.StatusCode -ge 200 -and $health.StatusCode -lt 300)
713
+ }
714
+ function Get-LocalReadyListenerOwners($Cfg) {
715
+ try { $uri = [Uri]([string]$Cfg.LocalReadyUri) } catch { Fail 'LocalReadyUri is invalid' }
716
+ if (-not $uri.IsLoopback) { Fail 'LocalReadyUri must be loopback for owned readiness attribution' }
717
+ try {
718
+ $listeners = @(Get-NetTCPConnection -State Listen -ErrorAction Stop | Where-Object {
719
+ [int]$_.LocalPort -eq [int]$uri.Port -and (
720
+ [string]$_.LocalAddress -eq [string]$uri.Host -or
721
+ [string]$_.LocalAddress -eq '0.0.0.0' -or
722
+ [string]$_.LocalAddress -eq '::'
723
+ )
724
+ })
725
+ } catch { Fail "cannot inspect listener ownership for local readiness endpoint: $($_.Exception.Message)" }
726
+ return @($listeners | ForEach-Object { [int]$_.OwningProcess } | Sort-Object -Unique)
727
+ }
728
+
729
+ function Assert-LocalReadyPortAvailable($Cfg) {
730
+ $owners = @(Get-LocalReadyListenerOwners $Cfg)
731
+ if ($owners.Count -gt 0) {
732
+ Fail ("local readiness endpoint already has listener pid(s)={0}; refusing to start an unverified Bridge" -f ($owners -join ','))
733
+ }
734
+ }
735
+
736
+ function Test-LocalReadyOwned($Cfg, $Bridge) {
737
+ if (-not $Bridge -or -not $Bridge.Alive) { return $false }
738
+ $owners = @(Get-LocalReadyListenerOwners $Cfg)
739
+ if ($owners.Count -ne 1 -or $owners[0] -ne [int]$Bridge.Ledger.pid) { return $false }
740
+ return (Test-LocalReady $Cfg)
741
+ }
742
+ function Get-LocalHealthOwned($Cfg, $Bridge) {
743
+ if (-not $Bridge -or -not $Bridge.Alive) { return [pscustomobject]@{ State='UNREACHABLE'; StatusCode=0; Diagnostic='bridge not alive' } }
744
+ $owners = @(Get-LocalReadyListenerOwners $Cfg)
745
+ if ($owners.Count -ne 1 -or $owners[0] -ne [int]$Bridge.Ledger.pid) { return [pscustomobject]@{ State='UNATTRIBUTED'; StatusCode=0; Diagnostic='readiness listener ownership mismatch' } }
746
+ return (Get-LocalHealth $Cfg)
747
+ }
748
+
749
+ function Get-ObservedState($Cfg) {
750
+ $bridge = Test-OwnedInstance 'Bridge'
751
+ $cloud = Test-OwnedInstance 'Cloud'
752
+ $core = Get-DownstreamCoreObservation $Cfg $bridge
753
+ $health = if ($bridge.Alive -and $core.alive) { try { Get-LocalHealthOwned $Cfg $bridge } catch { [pscustomobject]@{ State='UNREACHABLE'; StatusCode=0; Diagnostic='readiness attribution failed' } } } else { [pscustomobject]@{ State='UNREACHABLE'; StatusCode=0; Diagnostic='core not alive' } }
754
+ $ready = $health.State -eq 'READY'
755
+ $state = if (-not $bridge.Alive -and -not $cloud.Alive) { 'OFFLINE' }
756
+ elseif (-not $bridge.Alive -and $cloud.Alive) { 'PARKED' }
757
+ elseif ($bridge.Alive -and $cloud.Alive -and $ready) { 'ONLINE' }
758
+ elseif ($bridge.Alive -and $cloud.Alive -and $core.alive -and $health.State -eq 'DEGRADED') { 'DEGRADED' }
759
+ elseif ($bridge.Alive -and $core.reason -eq 'startup-zero-child') { 'STARTING' }
760
+ else { 'FAILED' }
761
+ [pscustomobject]@{ State=$state; Bridge=$bridge; Core=$core; Cloud=$cloud; LocalReady=$ready; LocalHealth=$health }
762
+ }
763
+
764
+ function Wait-LocalOperational($Cfg) {
765
+ $deadline = [DateTime]::UtcNow.AddSeconds([int]$Cfg.StartTimeoutSeconds)
766
+ do {
767
+ $bridge = Test-OwnedInstance 'Bridge'
768
+ if (-not $bridge.Alive) { return $null }
769
+ $core = Get-DownstreamCoreObservation $Cfg $bridge
770
+ if (-not $core.alive -and $core.reason -ne 'startup-zero-child') { return $null }
771
+ if ($core.alive) {
772
+ $health = Get-LocalHealthOwned $Cfg $bridge
773
+ if ($health.State -in @('READY','DEGRADED')) { return $health }
774
+ }
775
+ if ([DateTime]::UtcNow -ge $deadline) { return $null }
776
+ Start-Sleep -Milliseconds 250
777
+ } while ([DateTime]::UtcNow -lt $deadline)
778
+ return $null
779
+ }
780
+
781
+ function Wait-LocalReady($Cfg) {
782
+ $deadline = [DateTime]::UtcNow.AddSeconds([int]$Cfg.StartTimeoutSeconds)
783
+ do {
784
+ $bridge = Test-OwnedInstance 'Bridge'
785
+ if (-not $bridge.Alive) { return $false }
786
+ $core = Get-DownstreamCoreObservation $Cfg $bridge
787
+ if (-not $core.alive -and $core.reason -ne 'startup-zero-child') { return $false }
788
+ if ($core.alive -and (Test-LocalReadyOwned $Cfg $bridge)) { return $true }
789
+ if ([DateTime]::UtcNow -ge $deadline) { return $false }
790
+ Start-Sleep -Milliseconds 250
791
+ } while ([DateTime]::UtcNow -lt $deadline)
792
+ return $false
793
+ }
794
+
795
+ function Wait-PublicDnsReady($Cfg, [string]$Url) {
796
+ try { $uri = [Uri]$Url } catch { Fail 'public MCP URL is invalid' }
797
+ if ($uri.Scheme -ne 'https') { Fail 'public MCP URL must be HTTPS' }
798
+ $deadline = [DateTime]::UtcNow.AddSeconds([int]$Cfg.StartTimeoutSeconds)
799
+ $requiredConsecutive = 3
800
+ $consecutive = 0
801
+ do {
802
+ try {
803
+ $addresses = @([System.Net.Dns]::GetHostAddresses($uri.DnsSafeHost))
804
+ if ($addresses.Count -gt 0) { $consecutive += 1 } else { $consecutive = 0 }
805
+ } catch { $consecutive = 0 }
806
+ if ($consecutive -ge $requiredConsecutive) { return }
807
+ Start-Sleep -Milliseconds 750
808
+ } while ([DateTime]::UtcNow -lt $deadline)
809
+ Fail 'public endpoint DNS did not become stably resolvable before gate'
810
+ }
811
+
812
+ function Get-CurrentUrl($Cfg) {
813
+ if ($Cfg.ContainsKey('McpUrlFile') -and -not [string]::IsNullOrWhiteSpace([string]$Cfg.McpUrlFile)) {
814
+ $fullFile = [string]$Cfg.McpUrlFile
815
+ if (-not (Test-Path -LiteralPath $fullFile -PathType Leaf)) { Fail "McpUrlFile missing: $fullFile" }
816
+ $transport = if ($Cfg.Cloud.ContainsKey('Transport')) { [string]$Cfg.Cloud.Transport } else { '' }
817
+ if ($transport -eq 'deadbyte.named-tunnel.v2') {
818
+ if (-not $Cfg.ContainsKey('McpUrlFileSha256') -or [string]$Cfg.McpUrlFileSha256 -cnotmatch '^[0-9a-f]{64}$') { Fail 'deadbyte.named-tunnel.v2 requires McpUrlFileSha256' }
819
+ $fullHash = Get-ExecutableHash $fullFile
820
+ if ($fullHash -ne [string]$Cfg.McpUrlFileSha256) { Fail "McpUrlFile SHA-256 mismatch: expected $($Cfg.McpUrlFileSha256), found $fullHash" }
821
+ }
822
+ $full = (Read-TextFileSafe $fullFile).Trim()
823
+ if ($full -notmatch '^https://') { Fail 'McpUrlFile must contain one HTTPS MCP endpoint' }
824
+ if ($transport -eq 'deadbyte.named-tunnel.v2') {
825
+ try { $fullUri = [System.Uri]$full } catch { Fail 'McpUrlFile must contain one valid HTTPS MCP endpoint' }
826
+ if ($fullUri.DnsSafeHost.ToLowerInvariant().EndsWith('.trycloudflare.com')) { Fail 'trycloudflare endpoint forbidden for deadbyte.named-tunnel.v2' }
827
+ }
828
+ return $full
829
+ }
830
+ $cloud = Test-OwnedInstance 'Cloud'
831
+ if (-not $cloud.Alive) { return $null }
832
+ $originFile = Join-Path $RuntimeDir 'current-origin.txt'
833
+ $origin = (Read-TextFileSafe $originFile).Trim()
834
+ if (-not $origin) {
835
+ $pattern = [string]$Cfg.UrlRegex
836
+ foreach ($file in @((Log-Path 'Cloud' 'stdout'), (Log-Path 'Cloud' 'stderr'))) {
837
+ $text = Read-TextFileSafe $file
838
+ $m = [regex]::Matches($text, $pattern)
839
+ if ($m.Count -gt 0) { $origin = $m[$m.Count - 1].Value }
840
+ }
841
+ if (-not $origin) { return $null }
842
+ Set-Content -LiteralPath $originFile -Value $origin -Encoding ASCII
843
+ }
844
+ $suffix = ''
845
+ if ($Cfg.ContainsKey('McpPathFile') -and -not [string]::IsNullOrWhiteSpace([string]$Cfg.McpPathFile)) {
846
+ $pathFile = [string]$Cfg.McpPathFile
847
+ if (-not (Test-Path -LiteralPath $pathFile -PathType Leaf)) { Fail "McpPathFile missing: $pathFile" }
848
+ $suffix = (Read-TextFileSafe $pathFile).Trim()
849
+ if ($suffix -and -not $suffix.StartsWith('/')) { Fail 'MCP path file must contain an absolute URL path beginning with slash' }
850
+ if ($suffix -match '[\r\n]') { Fail 'MCP path file contains newline data' }
851
+ }
852
+ return $origin.TrimEnd('/') + $suffix
853
+ }
854
+
855
+ function Wait-CurrentUrl($Cfg) {
856
+ $deadline = [DateTime]::UtcNow.AddSeconds([int]$Cfg.StartTimeoutSeconds)
857
+ do {
858
+ $cloud = Test-OwnedInstance 'Cloud'
859
+ if (-not $cloud.Alive) { return $null }
860
+ $url = Get-CurrentUrl $Cfg
861
+ if ($url) { return $url }
862
+ Start-Sleep -Milliseconds 250
863
+ } while ([DateTime]::UtcNow -lt $deadline)
864
+ return $null
865
+ }
866
+
867
+ function Invoke-CoreGate($Cfg, [string]$ScriptName, [string[]]$ExtraArguments = @()) {
868
+ $coreRoot = [string]$Cfg.CoreRoot
869
+ $script = Join-Path (Join-Path $coreRoot 'src') $ScriptName
870
+ if (-not (Test-Path -LiteralPath $script -PathType Leaf)) { Fail "core gate script missing: $script" }
871
+ $node = (Get-Command node.exe -ErrorAction Stop).Source
872
+ $stdout = Log-Path 'CoreGate' 'stdout'
873
+ $stderr = Log-Path 'CoreGate' 'stderr'
874
+ Remove-Item -LiteralPath $stdout,$stderr -Force -ErrorAction SilentlyContinue
875
+ $argv = @(('"{0}"' -f $script))
876
+ foreach ($arg in $ExtraArguments) {
877
+ if ($arg -match '[\r\n"]') { Fail 'core gate argument contains forbidden quoting/newline data' }
878
+ $argv += ('"{0}"' -f $arg)
879
+ }
880
+ $proc = Start-Process -FilePath $node -ArgumentList $argv -WorkingDirectory $coreRoot -Wait -PassThru -WindowStyle Hidden -RedirectStandardOutput $stdout -RedirectStandardError $stderr
881
+ if ($proc.ExitCode -ne 0) {
882
+ $detail = if (Test-Path -LiteralPath $stderr) { (Get-Content -LiteralPath $stderr -Raw -Encoding UTF8).Trim() } else { '' }
883
+ Fail "core gate $ScriptName failed exit=$($proc.ExitCode): $detail"
884
+ }
885
+ Write-Host (Get-Content -LiteralPath $stdout -Raw -Encoding UTF8)
886
+ }
887
+
888
+ function Start-Deadbyte($Cfg) {
889
+ $state = Get-ObservedState $Cfg
890
+ if ($state.State -in @('ONLINE','DEGRADED')) { return $state }
891
+ if ($state.State -in @('FAILED','STARTING')) { Fail "runtime is $($state.State); inspect status and reconcile verified owned components before restart" }
892
+ $cloudWasAlive = [bool]$state.Cloud.Alive
893
+ Assert-LocalReadyPortAvailable $Cfg
894
+ try {
895
+ Start-OwnedComponent 'Bridge' $Cfg.Bridge $Cfg | Out-Null
896
+ $localHealth = Wait-LocalOperational $Cfg
897
+ if ($null -eq $localHealth) { Fail 'bridge started but owned local readiness gate failed' }
898
+ Start-OwnedComponent 'Cloud' $Cfg.Cloud $Cfg | Out-Null
899
+ if (-not (Wait-CurrentUrl $Cfg)) { Fail 'public endpoint metadata did not become available before timeout' }
900
+ $final = Get-ObservedState $Cfg
901
+ if ($final.State -notin @('ONLINE','DEGRADED')) { Fail "start gate failed: observed $($final.State)" }
902
+ return $final
903
+ } catch {
904
+ try { Stop-OwnedComponent 'Bridge' } catch {}
905
+ if (-not $cloudWasAlive) { try { Stop-OwnedComponent 'Cloud' } catch {} }
906
+ throw
907
+ }
908
+ }
909
+
910
+ function Park-Deadbyte($Cfg) {
911
+ Disarm-Desktop $Cfg -Quiet
912
+ Stop-OwnedComponent 'Bridge'
913
+ $s = Get-ObservedState $Cfg
914
+ if ($s.State -notin @('PARKED','OFFLINE')) { Fail "park gate failed: observed $($s.State)" }
915
+ return $s
916
+ }
917
+
918
+ function Stop-Deadbyte($Cfg) {
919
+ Disarm-Desktop $Cfg -Quiet
920
+ Stop-OwnedComponent 'Bridge'
921
+ Stop-OwnedComponent 'Cloud'
922
+ Remove-Item -LiteralPath (Join-Path $RuntimeDir 'current-origin.txt') -Force -ErrorAction SilentlyContinue
923
+ $s = Get-ObservedState $Cfg
924
+ if ($s.State -ne 'OFFLINE') { Fail "stop gate failed: observed $($s.State)" }
925
+ return $s
926
+ }
927
+
928
+ function Assert-PolicyRootsAvailable($Policy, [string]$Plane) {
929
+ $rootIds = @($Policy.roots.PSObject.Properties.Name)
930
+ if ($rootIds.Count -lt 1) { Fail "$Plane policy requires at least one root" }
931
+ foreach ($rootId in $rootIds) {
932
+ $raw = [string]$Policy.roots.$rootId.path
933
+ if ([string]::IsNullOrWhiteSpace($raw)) { Fail "$Plane policy root '$rootId' path missing" }
934
+ try { $full = [System.IO.Path]::GetFullPath($raw) } catch { Fail "$Plane policy root '$rootId' path invalid" }
935
+ if (-not (Test-Path -LiteralPath $full -PathType Container)) { Fail "$Plane policy root '$rootId' missing: $full" }
936
+ }
937
+ return $rootIds
938
+ }
939
+
940
+ function Get-AuthorityEpochStatus($Cfg, [string]$Plane, [string]$GrantFile, [string]$GrantNonce) {
941
+ if ([string]::IsNullOrWhiteSpace($GrantNonce) -or $GrantNonce -cnotmatch '^[0-9a-f]{64}$') {
942
+ return [pscustomobject]@{ Trusted=$false; Reason='epoch-grant-nonce-invalid'; Seq=$null; Action=$null; EventSha256=$null }
943
+ }
944
+ try {
945
+ $verified = Invoke-AuthorityEpochCommand $Cfg $Plane 'status' $GrantFile
946
+ if (-not $verified.trusted) {
947
+ return [pscustomobject]@{ Trusted=$false; Reason=[string]$verified.reason; Seq=$verified.seq; Action=$verified.action; EventSha256=$verified.event_sha256 }
948
+ }
949
+ return [pscustomobject]@{ Trusted=$true; Reason=[string]$verified.reason; Seq=$verified.seq; Action=$verified.action; EventSha256=$verified.event_sha256 }
950
+ } catch {
951
+ return [pscustomobject]@{ Trusted=$false; Reason='authority-epoch-verifier-unavailable'; Seq=$null; Action=$null; EventSha256=$null }
952
+ }
953
+ }
954
+
955
+ function Invoke-AuthorityEpochCommand($Cfg, [string]$Plane, [string]$Action, [string]$GrantFile, [string]$UpdateId = '', [string]$ExpectedHead = '') {
956
+ $cli = Join-Path ([string]$Cfg.CoreRoot) 'src\authority-epoch-cli-r65.mjs'
957
+ if (-not (Test-Path -LiteralPath $cli -PathType Leaf)) { Fail "authority epoch CLI missing: $cli" }
958
+ $node = [string]$Cfg.Bridge.FilePath
959
+ if ([string]::IsNullOrWhiteSpace($node) -or -not (Test-Path -LiteralPath $node -PathType Leaf)) { Fail 'authority epoch Node executable unavailable' }
960
+ $policyFile = switch ($Plane) {
961
+ 'process' { [string]$Cfg.ProcessPolicyFile }
962
+ 'coding' { [string]$Cfg.CodingPolicyFile }
963
+ 'autonomous' { [string]$Cfg.AutonomousPolicyFile }
964
+ 'desktop' { [string]$Cfg.DesktopPolicyFile }
965
+ 'host' { [string]$Cfg.HostPolicyFile }
966
+ default { Fail "authority epoch plane invalid: $Plane" }
967
+ }
968
+ if ([string]::IsNullOrWhiteSpace($policyFile) -or -not (Test-Path -LiteralPath $policyFile -PathType Leaf)) { Fail "authority epoch policy unavailable for $Plane" }
969
+ $args = @($cli, $Action, '--plane', $Plane, '--grant-file', $GrantFile, '--policy-file', $policyFile)
970
+ if ($Action -in @('transaction-revoke','transaction-restore')) {
971
+ if ([string]::IsNullOrWhiteSpace($UpdateId) -or $ExpectedHead -cnotmatch '^[0-9a-f]{64}$') { Fail 'authority epoch transaction binding invalid' }
972
+ $args += @('--update-id', $UpdateId, '--expected-head', $ExpectedHead)
973
+ }
974
+ $output = @(& $node @args)
975
+ $exit = $LASTEXITCODE
976
+ if ($exit -ne 0) { Fail "authority epoch command '$Action' failed for $Plane (exit=$exit)" }
977
+ try { return (($output -join [Environment]::NewLine) | ConvertFrom-Json) }
978
+ catch { Fail "authority epoch command '$Action' returned invalid JSON for $Plane" }
979
+ }
980
+
981
+ function Get-HostPolicyMetadata($Cfg) {
982
+ if ([string]::IsNullOrWhiteSpace([string]$Cfg.HostPolicyFile)) { Fail 'privileged host plane is not configured' }
983
+ $policyPath = [string]$Cfg.HostPolicyFile
984
+ try { $policy = Get-Content -LiteralPath $policyPath -Raw -Encoding UTF8 | ConvertFrom-Json } catch { Fail "invalid host policy JSON: $policyPath" }
985
+ if (@('deadbyte.host-policy.v1','deadbyte.host-policy.v2') -notcontains [string]$policy.schema -or $policy.enabled -ne $true) { Fail 'host policy schema/enabled mismatch' }
986
+ if ([string]::IsNullOrWhiteSpace([string]$policy.arm_file)) { Fail 'host policy arm_file missing' }
987
+ $armFile = [System.IO.Path]::GetFullPath([string]$policy.arm_file)
988
+ $runtimeReal = [System.IO.Path]::GetFullPath($RuntimeDir).TrimEnd('\') + '\'
989
+ if (-not $armFile.StartsWith($runtimeReal, [System.StringComparison]::OrdinalIgnoreCase)) { Fail 'host policy arm_file must live under controller runtime directory' }
990
+ Assert-PolicyRootsAvailable $policy 'host' | Out-Null
991
+ [pscustomobject]@{
992
+ Path = $policyPath
993
+ Sha256 = Get-ExecutableHash $policyPath
994
+ ArmFile = $armFile
995
+ }
996
+ }
997
+
998
+ function Get-HostArmStatus($Cfg) {
999
+ if ([string]::IsNullOrWhiteSpace([string]$Cfg.HostPolicyFile)) { return [pscustomobject]@{ Configured=$false; Armed=$false; Reason='no-policy' } }
1000
+ try { $meta = Get-HostPolicyMetadata $Cfg } catch { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason=$_.Exception.Message } }
1001
+ if (-not (Test-Path -LiteralPath $meta.ArmFile -PathType Leaf)) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='disarmed' } }
1002
+ try { $arm = Get-Content -LiteralPath $meta.ArmFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='invalid-grant' } }
1003
+ if ($arm.schema -ne 'deadbyte.host-grant.v2') { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='grant-schema' } }
1004
+ if ([string]$arm.policy_sha256 -ne [string]$meta.Sha256) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='policy-digest' } }
1005
+ if ([string]$arm.instance_nonce -cnotmatch '^[0-9a-f]{64}$') { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='nonce-invalid' } }
1006
+ $epoch = Get-AuthorityEpochStatus $Cfg 'host' $meta.ArmFile ([string]$arm.instance_nonce)
1007
+ if (-not $epoch.Trusted) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason=[string]$epoch.Reason } }
1008
+ return [pscustomobject]@{ Configured=$true; Armed=$true; Reason='standing-grant'; EpochSeq=$epoch.Seq; EpochSha256=$epoch.EventSha256 }
1009
+ }
1010
+
1011
+ function Arm-Host($Cfg) {
1012
+ $meta = Get-HostPolicyMetadata $Cfg
1013
+ Ensure-Directory $RuntimeDir
1014
+ $nonceBytes = New-Object byte[] 32
1015
+ $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
1016
+ try { $rng.GetBytes($nonceBytes) } finally { $rng.Dispose() }
1017
+ $nonce = -join ($nonceBytes | ForEach-Object { $_.ToString('x2') })
1018
+ $now = [DateTime]::UtcNow
1019
+ $body = [ordered]@{
1020
+ schema = 'deadbyte.host-grant.v2'
1021
+ policy_sha256 = [string]$meta.Sha256
1022
+ instance_nonce = $nonce
1023
+ granted_at_utc = $now.ToString('o')
1024
+ }
1025
+ $tmp = $meta.ArmFile + '.tmp'
1026
+ $body | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $tmp -Encoding UTF8
1027
+ Move-Item -LiteralPath $tmp -Destination $meta.ArmFile -Force
1028
+ try { Invoke-AuthorityEpochCommand $Cfg 'host' 'grant' $meta.ArmFile | Out-Null }
1029
+ catch {
1030
+ Remove-Item -LiteralPath $meta.ArmFile -Force -ErrorAction SilentlyContinue
1031
+ throw
1032
+ }
1033
+ Write-Host 'PRIVILEGED HOST PLANE ENABLED UNTIL EXPLICIT REVOKE'
1034
+ }
1035
+
1036
+ function Disarm-Host($Cfg) {
1037
+ $meta = Get-HostPolicyMetadata $Cfg
1038
+ $epochError = $null
1039
+ if (Test-Path -LiteralPath $meta.ArmFile) {
1040
+ try { Invoke-AuthorityEpochCommand $Cfg 'host' 'user-revoke' $meta.ArmFile | Out-Null } catch { $epochError = $_.Exception }
1041
+ Remove-Item -LiteralPath $meta.ArmFile -Force -ErrorAction SilentlyContinue
1042
+ }
1043
+ if (Test-Path -LiteralPath $meta.ArmFile) { throw 'Host authority revocation failed: grant remains present' }
1044
+ if ($null -ne $epochError) { throw $epochError }
1045
+ Write-Host 'PRIVILEGED HOST PLANE DISARMED'
1046
+ }
1047
+
1048
+ function Show-HostStatus($Cfg) {
1049
+ $h = Get-HostArmStatus $Cfg
1050
+ Write-Host ("Host plane : {0} [{1}]" -f $(if ($h.Armed) { 'ARMED' } elseif ($h.Configured) { 'DISARMED' } else { 'UNCONFIGURED' }), $h.Reason)
1051
+ return $h
1052
+ }
1053
+
1054
+ function Get-ProcessPolicyMetadata($Cfg) {
1055
+ if ([string]::IsNullOrWhiteSpace([string]$Cfg.ProcessPolicyFile)) { Fail 'process plane is not configured' }
1056
+ $policyPath = [string]$Cfg.ProcessPolicyFile
1057
+ try { $policy = Get-Content -LiteralPath $policyPath -Raw -Encoding UTF8 | ConvertFrom-Json } catch { Fail "invalid process policy JSON: $policyPath" }
1058
+ if ($policy.schema -ne 'deadbyte.process-policy.v1' -or $policy.enabled -ne $true) { Fail 'process policy schema/enabled mismatch' }
1059
+ if ([string]::IsNullOrWhiteSpace([string]$policy.grant_file)) { Fail 'process policy grant_file missing' }
1060
+ $grantFile = [System.IO.Path]::GetFullPath([string]$policy.grant_file)
1061
+ $runtimeReal = [System.IO.Path]::GetFullPath($RuntimeDir).TrimEnd('\') + '\'
1062
+ if (-not $grantFile.StartsWith($runtimeReal, [System.StringComparison]::OrdinalIgnoreCase)) { Fail 'process policy grant_file must live under controller runtime directory' }
1063
+ $profileIds = @($policy.profiles.PSObject.Properties.Name)
1064
+ $rootIds = @(Assert-PolicyRootsAvailable $policy 'process')
1065
+ if ($profileIds.Count -lt 1 -or $rootIds.Count -lt 1) { Fail 'process policy requires profiles and roots' }
1066
+ [pscustomobject]@{ Path=$policyPath; Sha256=Get-ExecutableHash $policyPath; GrantFile=$grantFile; ProfileIds=$profileIds; RootIds=$rootIds }
1067
+ }
1068
+
1069
+ function Get-ProcessGrantStatus($Cfg) {
1070
+ if ([string]::IsNullOrWhiteSpace([string]$Cfg.ProcessPolicyFile)) { return [pscustomobject]@{ Configured=$false; Armed=$false; Reason='no-policy' } }
1071
+ try { $meta = Get-ProcessPolicyMetadata $Cfg } catch { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason=$_.Exception.Message } }
1072
+ if (-not (Test-Path -LiteralPath $meta.GrantFile -PathType Leaf)) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='disarmed' } }
1073
+ try { $grant = Get-Content -LiteralPath $meta.GrantFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='invalid-grant' } }
1074
+ if ($grant.schema -ne 'deadbyte.process-grant.v1') { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='grant-schema' } }
1075
+ if ([string]$grant.policy_sha256 -ne [string]$meta.Sha256) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='policy-digest' } }
1076
+ if ([string]$grant.instance_nonce -cnotmatch '^[0-9a-f]{64}$') { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='nonce-invalid' } }
1077
+ $profiles = @($grant.profile_ids); $roots = @($grant.root_ids)
1078
+ if ($profiles.Count -lt 1 -or @($profiles | Where-Object { $_ -notin $meta.ProfileIds }).Count -gt 0) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='profiles-invalid' } }
1079
+ if ($roots.Count -lt 1 -or @($roots | Where-Object { $_ -notin $meta.RootIds }).Count -gt 0) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='roots-invalid' } }
1080
+ $epoch = Get-AuthorityEpochStatus $Cfg 'process' $meta.GrantFile ([string]$grant.instance_nonce)
1081
+ if (-not $epoch.Trusted) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason=[string]$epoch.Reason } }
1082
+ return [pscustomobject]@{ Configured=$true; Armed=$true; Reason='standing-grant'; EpochSeq=$epoch.Seq; EpochSha256=$epoch.EventSha256 }
1083
+ }
1084
+
1085
+ function Arm-Process($Cfg) {
1086
+ $meta = Get-ProcessPolicyMetadata $Cfg
1087
+ Ensure-Directory $RuntimeDir
1088
+ $nonceBytes = New-Object byte[] 32
1089
+ $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
1090
+ try { $rng.GetBytes($nonceBytes) } finally { $rng.Dispose() }
1091
+ $nonce = -join ($nonceBytes | ForEach-Object { $_.ToString('x2') })
1092
+ $body = [ordered]@{
1093
+ schema = 'deadbyte.process-grant.v1'
1094
+ policy_sha256 = [string]$meta.Sha256
1095
+ instance_nonce = $nonce
1096
+ profile_ids = @($meta.ProfileIds)
1097
+ root_ids = @($meta.RootIds)
1098
+ granted_at_utc = [DateTime]::UtcNow.ToString('o')
1099
+ }
1100
+ $tmp = $meta.GrantFile + '.tmp'
1101
+ $body | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $tmp -Encoding UTF8
1102
+ Move-Item -LiteralPath $tmp -Destination $meta.GrantFile -Force
1103
+ try { Invoke-AuthorityEpochCommand $Cfg 'process' 'grant' $meta.GrantFile | Out-Null }
1104
+ catch {
1105
+ Remove-Item -LiteralPath $meta.GrantFile -Force -ErrorAction SilentlyContinue
1106
+ throw
1107
+ }
1108
+ Write-Host 'PROCESS PLANE ENABLED UNTIL EXPLICIT REVOKE'
1109
+ }
1110
+
1111
+ function Disarm-Process($Cfg) {
1112
+ $meta = Get-ProcessPolicyMetadata $Cfg
1113
+ $epochError = $null
1114
+ if (Test-Path -LiteralPath $meta.GrantFile -PathType Leaf) {
1115
+ try { Invoke-AuthorityEpochCommand $Cfg 'process' 'user-revoke' $meta.GrantFile | Out-Null } catch { $epochError = $_.Exception }
1116
+ Remove-Item -LiteralPath $meta.GrantFile -Force -ErrorAction SilentlyContinue
1117
+ }
1118
+ if (Test-Path -LiteralPath $meta.GrantFile) { throw 'Process authority revocation failed: grant remains present' }
1119
+ if ($null -ne $epochError) { throw $epochError }
1120
+ Write-Host 'PROCESS PLANE DISARMED'
1121
+ }
1122
+
1123
+ function Show-ProcessStatus($Cfg) {
1124
+ $p = Get-ProcessGrantStatus $Cfg
1125
+ Write-Host ("Process plane : {0} [{1}]" -f $(if ($p.Armed) { 'ARMED' } elseif ($p.Configured) { 'DISARMED' } else { 'UNCONFIGURED' }), $p.Reason)
1126
+ return $p
1127
+ }
1128
+
1129
+ function Read-ControllerPolicySnapshot([string]$PolicyPath, [string]$Kind) {
1130
+ try {
1131
+ $bytes = [System.IO.File]::ReadAllBytes($PolicyPath)
1132
+ $decoder = New-Object System.Text.UTF8Encoding($false, $true)
1133
+ $text = $decoder.GetString($bytes)
1134
+ if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
1135
+ $policy = $text | ConvertFrom-Json
1136
+ } catch { Fail "invalid $Kind policy JSON: $PolicyPath" }
1137
+ $sha = [System.Security.Cryptography.SHA256]::Create()
1138
+ try { $digest = ([System.BitConverter]::ToString($sha.ComputeHash($bytes))).Replace('-', '').ToLowerInvariant() }
1139
+ finally { $sha.Dispose() }
1140
+ return [pscustomobject]@{ Policy=$policy; Sha256=$digest }
1141
+ }
1142
+
1143
+ function Get-CodingPolicyMetadata($Cfg) {
1144
+ if ([string]::IsNullOrWhiteSpace([string]$Cfg.CodingPolicyFile)) { Fail 'coding plane is not configured' }
1145
+ $policyPath = [string]$Cfg.CodingPolicyFile
1146
+ $snapshot = Read-ControllerPolicySnapshot $policyPath 'coding'
1147
+ $policy = $snapshot.Policy
1148
+ if ($policy.schema -ne 'deadbyte.coding-policy.v1' -or $policy.enabled -ne $true) { Fail 'coding policy schema/enabled mismatch' }
1149
+ if ([string]::IsNullOrWhiteSpace([string]$policy.lease_file)) { Fail 'coding policy lease_file missing' }
1150
+ $leaseFile = [System.IO.Path]::GetFullPath([string]$policy.lease_file)
1151
+ $runtimeReal = [System.IO.Path]::GetFullPath($RuntimeDir).TrimEnd('\') + '\'
1152
+ if (-not $leaseFile.StartsWith($runtimeReal, [System.StringComparison]::OrdinalIgnoreCase)) { Fail 'coding policy lease_file must live under controller runtime directory' }
1153
+ $rootIds = @(Assert-PolicyRootsAvailable $policy 'coding')
1154
+ $rootPaths = @{}
1155
+ $rootAccess = @{}
1156
+ foreach ($rootId in $rootIds) {
1157
+ $root = $policy.roots.$rootId
1158
+ $rootPaths[$rootId] = [System.IO.Path]::GetFullPath([string]$root.path)
1159
+ $rootAccess[$rootId] = [pscustomobject]@{ Read=($root.read -eq $true); Write=($root.write -eq $true); Exec=($root.exec -eq $true) }
1160
+ }
1161
+ [pscustomobject]@{ Path=$policyPath; Sha256=$snapshot.Sha256; LeaseFile=$leaseFile; RootIds=$rootIds; RootPaths=$rootPaths; RootAccess=$rootAccess }
1162
+ }
1163
+
1164
+ function Get-CodingLeaseStatus($Cfg) {
1165
+ if ([string]::IsNullOrWhiteSpace([string]$Cfg.CodingPolicyFile)) { return [pscustomobject]@{ Configured=$false; Armed=$false; Reason='no-policy' } }
1166
+ try { $meta = Get-CodingPolicyMetadata $Cfg } catch { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason=$_.Exception.Message } }
1167
+ if (-not (Test-Path -LiteralPath $meta.LeaseFile -PathType Leaf)) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='disarmed' } }
1168
+ try { $lease = Get-Content -LiteralPath $meta.LeaseFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='invalid-grant' } }
1169
+ if ($lease.schema -ne 'deadbyte.coding-grant.v2') { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='grant-schema' } }
1170
+ if ([string]$lease.policy_sha256 -ne [string]$meta.Sha256) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='policy-digest' } }
1171
+ if ([string]$lease.instance_nonce -cnotmatch '^[0-9a-f]{64}$') { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='nonce-invalid' } }
1172
+ $roots = @($lease.root_ids)
1173
+ if ($roots.Count -lt 1 -or @($roots | Where-Object { $_ -notin $meta.RootIds }).Count -gt 0) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='roots-invalid' } }
1174
+ $epoch = Get-AuthorityEpochStatus $Cfg 'coding' $meta.LeaseFile ([string]$lease.instance_nonce)
1175
+ if (-not $epoch.Trusted) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason=[string]$epoch.Reason } }
1176
+ return [pscustomobject]@{ Configured=$true; Armed=$true; Reason='standing-grant'; RootIds=$roots; EpochSeq=$epoch.Seq; EpochSha256=$epoch.EventSha256 }
1177
+ }
1178
+
1179
+ function Arm-Coding($Cfg) {
1180
+ $meta = Get-CodingPolicyMetadata $Cfg
1181
+ Ensure-Directory $RuntimeDir
1182
+ $nonceBytes = New-Object byte[] 32
1183
+ $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
1184
+ try { $rng.GetBytes($nonceBytes) } finally { $rng.Dispose() }
1185
+ $nonce = -join ($nonceBytes | ForEach-Object { $_.ToString('x2') })
1186
+ $now = [DateTime]::UtcNow
1187
+ $body = [ordered]@{
1188
+ schema = 'deadbyte.coding-grant.v2'
1189
+ policy_sha256 = [string]$meta.Sha256
1190
+ instance_nonce = $nonce
1191
+ root_ids = @($meta.RootIds)
1192
+ granted_at_utc = $now.ToString('o')
1193
+ }
1194
+ $tmp = $meta.LeaseFile + '.tmp'
1195
+ $body | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $tmp -Encoding UTF8
1196
+ Move-Item -LiteralPath $tmp -Destination $meta.LeaseFile -Force
1197
+ try { Invoke-AuthorityEpochCommand $Cfg 'coding' 'grant' $meta.LeaseFile | Out-Null }
1198
+ catch {
1199
+ Remove-Item -LiteralPath $meta.LeaseFile -Force -ErrorAction SilentlyContinue
1200
+ throw
1201
+ }
1202
+ Write-Host 'CODING PLANE ENABLED UNTIL EXPLICIT REVOKE'
1203
+ }
1204
+
1205
+ function Disarm-Coding($Cfg) {
1206
+ $meta = Get-CodingPolicyMetadata $Cfg
1207
+ $epochError = $null
1208
+ if (Test-Path -LiteralPath $meta.LeaseFile -PathType Leaf) {
1209
+ try { Invoke-AuthorityEpochCommand $Cfg 'coding' 'user-revoke' $meta.LeaseFile | Out-Null } catch { $epochError = $_.Exception }
1210
+ Remove-Item -LiteralPath $meta.LeaseFile -Force -ErrorAction SilentlyContinue
1211
+ }
1212
+ try { Disarm-Autonomous $Cfg -Quiet } catch {
1213
+ if ($null -eq $epochError) { $epochError = $_.Exception }
1214
+ else { Write-Warning "autonomous lease cleanup failed after coding revocation: $($_.Exception.Message)" }
1215
+ }
1216
+ if (Test-Path -LiteralPath $meta.LeaseFile) { throw 'Coding authority revocation failed: grant remains present' }
1217
+ if ($null -ne $epochError) { throw $epochError }
1218
+ Write-Host 'CODING PLANE DISARMED; AUTONOMOUS AUTHORITY REVOKED'
1219
+ }
1220
+
1221
+ function Show-CodingStatus($Cfg) {
1222
+ $c = Get-CodingLeaseStatus $Cfg
1223
+ Write-Host ("Coding plane : {0} [{1}]" -f $(if ($c.Armed) { 'ARMED' } elseif ($c.Configured) { 'DISARMED' } else { 'UNCONFIGURED' }), $c.Reason)
1224
+ return $c
1225
+ }
1226
+
1227
+ function Get-AutonomousPolicyMetadata($Cfg) {
1228
+ if ([string]::IsNullOrWhiteSpace([string]$Cfg.AutonomousPolicyFile)) { Fail 'autonomous plane is not configured' }
1229
+ $policyPath = [string]$Cfg.AutonomousPolicyFile
1230
+ $snapshot = Read-ControllerPolicySnapshot $policyPath 'autonomous'
1231
+ $policy = $snapshot.Policy
1232
+ if ($policy.schema -ne 'deadbyte.autonomous-policy.v1' -or $policy.enabled -ne $true) { Fail 'autonomous policy schema/enabled mismatch' }
1233
+ if ([string]::IsNullOrWhiteSpace([string]$policy.lease_file)) { Fail 'autonomous policy lease_file missing' }
1234
+ $leaseFile = [System.IO.Path]::GetFullPath([string]$policy.lease_file)
1235
+ $runtimeReal = [System.IO.Path]::GetFullPath($RuntimeDir).TrimEnd('\') + '\'
1236
+ if (-not $leaseFile.StartsWith($runtimeReal, [System.StringComparison]::OrdinalIgnoreCase)) { Fail 'autonomous policy lease_file must live under controller runtime directory' }
1237
+ $codingMeta = Get-CodingPolicyMetadata $Cfg
1238
+ if ([string]$policy.coding_policy_sha256 -ne [string]$codingMeta.Sha256) { Fail 'autonomous policy coding_policy_sha256 mismatch' }
1239
+ $rootIds = @($policy.root_ids)
1240
+ if ($rootIds.Count -lt 1) { Fail 'autonomous policy requires at least one root' }
1241
+ foreach ($rootId in $rootIds) {
1242
+ if ([string]$rootId -cnotin @($codingMeta.RootIds)) { Fail "autonomous policy root '$rootId' outside coding policy" }
1243
+ $rootPath = [string]$codingMeta.RootPaths[[string]$rootId]
1244
+ if ([string]::IsNullOrWhiteSpace($rootPath)) { Fail "autonomous policy root '$rootId' path missing" }
1245
+ $fullRoot = [System.IO.Path]::GetFullPath($rootPath).TrimEnd('\')
1246
+ $volumeRoot = [System.IO.Path]::GetPathRoot($rootPath).TrimEnd('\')
1247
+ if ($fullRoot.Equals($volumeRoot, [System.StringComparison]::OrdinalIgnoreCase)) { Fail 'autonomous bootstrap requires a project-scoped root' }
1248
+ try { $rootItem = Get-Item -LiteralPath $rootPath -Force -ErrorAction Stop }
1249
+ catch { Fail "autonomous policy root '$rootId' must be a real project directory" }
1250
+ if (-not $rootItem.PSIsContainer) { Fail "autonomous policy root '$rootId' must be a real project directory" }
1251
+ $ancestor = [System.IO.DirectoryInfo]$rootItem
1252
+ while ($null -ne $ancestor) {
1253
+ if (($ancestor.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { Fail "autonomous policy root '$rootId' must be a real project directory" }
1254
+ $ancestor = $ancestor.Parent
1255
+ }
1256
+ }
1257
+ if ($null -eq $codingMeta.RootAccess) { Fail 'coding policy root access metadata unavailable' }
1258
+ foreach ($codingRootId in @($codingMeta.RootIds)) {
1259
+ $access = $codingMeta.RootAccess[[string]$codingRootId]
1260
+ if ($null -eq $access) { Fail "coding policy root '$codingRootId' access metadata unavailable" }
1261
+ if ($access.Read -eq $true -or $access.Write -eq $true -or $access.Exec -eq $true) {
1262
+ $candidate = [string]$codingMeta.RootPaths[[string]$codingRootId]
1263
+ if ([string]::IsNullOrWhiteSpace($candidate)) { Fail "coding policy root '$codingRootId' path missing" }
1264
+ $candidateRoot = [System.IO.Path]::GetFullPath($candidate).TrimEnd('\')
1265
+ $filesystemRoot = [System.IO.Path]::GetPathRoot($candidate).TrimEnd('\')
1266
+ if ($candidateRoot.Equals($filesystemRoot, [System.StringComparison]::OrdinalIgnoreCase)) { Fail 'coding policy contains a broad filesystem root' }
1267
+ }
1268
+ }
1269
+ [pscustomobject]@{ Path=$policyPath; Sha256=$snapshot.Sha256; LeaseFile=$leaseFile; RootIds=$rootIds; CodingPolicySha256=$codingMeta.Sha256 }
1270
+ }
1271
+
1272
+ function Get-AutonomousLeaseStatus($Cfg) {
1273
+ if ([string]::IsNullOrWhiteSpace([string]$Cfg.AutonomousPolicyFile)) { return [pscustomobject]@{ Configured=$false; Armed=$false; Reason='no-policy' } }
1274
+ try { $meta = Get-AutonomousPolicyMetadata $Cfg } catch { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason=$_.Exception.Message } }
1275
+ $coding = Get-CodingLeaseStatus $Cfg
1276
+ if (-not $coding.Armed) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='coding-disarmed' } }
1277
+ if (-not (Test-Path -LiteralPath $meta.LeaseFile -PathType Leaf)) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='disarmed' } }
1278
+ try { $lease = Get-Content -LiteralPath $meta.LeaseFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='invalid-grant' } }
1279
+ if ($lease.schema -ne 'deadbyte.autonomous-grant.v2') { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='grant-schema' } }
1280
+ if ([string]$lease.policy_sha256 -ne [string]$meta.Sha256) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='policy-digest' } }
1281
+ if ([string]$lease.coding_policy_sha256 -ne [string]$meta.CodingPolicySha256) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='coding-policy-digest' } }
1282
+ if ([string]$lease.instance_nonce -cnotmatch '^[0-9a-f]{64}$') { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='nonce-invalid' } }
1283
+ $roots = @($lease.root_ids)
1284
+ if ($roots.Count -lt 1 -or @($roots | Where-Object { $_ -notin $meta.RootIds }).Count -gt 0) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='roots-invalid' } }
1285
+ if (@($roots | Where-Object { $_ -cnotin @($coding.RootIds) }).Count -gt 0) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='coding-roots' } }
1286
+ # R70 optional owner-bound expiry for v2 signed standing grants.
1287
+ # Legacy grants without an expiry field remain subject to explicit revoke.
1288
+ if ($null -ne $lease.PSObject.Properties['expires_at_utc']) {
1289
+ $issuedText = [string]$lease.granted_at_utc
1290
+ $expiryText = [string]$lease.expires_at_utc
1291
+ $timePattern = '^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3,7}Z$'
1292
+ if ($issuedText -cnotmatch $timePattern -or $expiryText -cnotmatch $timePattern) {
1293
+ return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='expiry-invalid' }
1294
+ }
1295
+ try {
1296
+ $issuedAt = [DateTimeOffset]::Parse($issuedText,[System.Globalization.CultureInfo]::InvariantCulture,[System.Globalization.DateTimeStyles]::RoundtripKind)
1297
+ $expiresAt = [DateTimeOffset]::Parse($expiryText,[System.Globalization.CultureInfo]::InvariantCulture,[System.Globalization.DateTimeStyles]::RoundtripKind)
1298
+ } catch {
1299
+ return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='expiry-invalid' }
1300
+ }
1301
+ $duration = ($expiresAt - $issuedAt).TotalMilliseconds
1302
+ if ($duration -le 0 -or $duration -gt 3600000) {
1303
+ return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='expiry-window-invalid' }
1304
+ }
1305
+ if ([DateTimeOffset]::UtcNow -lt $issuedAt) {
1306
+ return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='not-yet-valid' }
1307
+ }
1308
+ if ([DateTimeOffset]::UtcNow -ge $expiresAt) {
1309
+ return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='expired' }
1310
+ }
1311
+ }
1312
+ $epoch = Get-AuthorityEpochStatus $Cfg 'autonomous' $meta.LeaseFile ([string]$lease.instance_nonce)
1313
+ if (-not $epoch.Trusted) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason=[string]$epoch.Reason } }
1314
+ return [pscustomobject]@{ Configured=$true; Armed=$true; Reason='standing-grant'; EpochSeq=$epoch.Seq; EpochSha256=$epoch.EventSha256 }
1315
+ }
1316
+
1317
+ function Arm-Autonomous($Cfg) {
1318
+ $coding = Get-CodingLeaseStatus $Cfg
1319
+ if (-not $coding.Armed) { Fail 'autonomous plane requires an active coding lease; run armcoding first' }
1320
+ $meta = Get-AutonomousPolicyMetadata $Cfg
1321
+ if (@($meta.RootIds | Where-Object { $_ -cnotin @($coding.RootIds) }).Count -gt 0) { Fail 'autonomous roots exceed active coding grant' }
1322
+ # R70 migration fence: updater approval is NOT controller-side grant approval.
1323
+ # Until independent owner enrollment is verified at grant issuance, only
1324
+ # the byte-identical pre-R70 known-good Autonomous policy can be armed.
1325
+ # Root labels and self-asserted approval flags are never consent evidence.
1326
+ $legacyOwnerPolicySha256 = '6274492644c303b38117541cc22a59e88e6ff5fdef7051a5d57ad336f45765f5'
1327
+ $selectedRoots = @($meta.RootIds)
1328
+ if ([string]$meta.Sha256 -cne $legacyOwnerPolicySha256 -or $selectedRoots.Count -ne 1 -or [string]$selectedRoots[0] -cne 'core') {
1329
+ Fail 'R70_OWNER_ENROLLMENT_REQUIRED: project/successor Autonomous grant needs independent owner authorization before arm'
1330
+ }
1331
+ Ensure-Directory $RuntimeDir
1332
+ $nonceBytes = New-Object byte[] 32
1333
+ $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
1334
+ try { $rng.GetBytes($nonceBytes) } finally { $rng.Dispose() }
1335
+ $nonce = -join ($nonceBytes | ForEach-Object { $_.ToString('x2') })
1336
+ $now = [DateTime]::UtcNow
1337
+ $body = [ordered]@{ schema='deadbyte.autonomous-grant.v2'; policy_sha256=[string]$meta.Sha256; coding_policy_sha256=[string]$meta.CodingPolicySha256; instance_nonce=$nonce; root_ids=@($meta.RootIds); granted_at_utc=$now.ToString('o'); expires_at_utc=$now.AddHours(1).ToString('o') }
1338
+ $tmp = $meta.LeaseFile + '.tmp'
1339
+ $body | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $tmp -Encoding UTF8
1340
+ Move-Item -LiteralPath $tmp -Destination $meta.LeaseFile -Force
1341
+ try { Invoke-AuthorityEpochCommand $Cfg 'autonomous' 'grant' $meta.LeaseFile | Out-Null }
1342
+ catch {
1343
+ Remove-Item -LiteralPath $meta.LeaseFile -Force -ErrorAction SilentlyContinue
1344
+ throw
1345
+ }
1346
+ Write-Host 'AUTONOMOUS PLANE ENABLED UNTIL EXPLICIT REVOKE'
1347
+ }
1348
+
1349
+ function Get-AutonomousRevocationLeaseFile($Cfg) {
1350
+ if ([string]::IsNullOrWhiteSpace([string]$Cfg.AutonomousPolicyFile)) { return $null }
1351
+ # Revocation may not depend on approval/root checks used only for issuing grants.
1352
+ # The source remains the bound policy snapshot, never a caller-supplied path.
1353
+ $snapshot = Read-ControllerPolicySnapshot ([string]$Cfg.AutonomousPolicyFile) 'autonomous'
1354
+ $policy = $snapshot.Policy
1355
+ if ($policy.schema -ne 'deadbyte.autonomous-policy.v1' -or $policy.enabled -ne $true) { Fail 'autonomous policy schema/enabled mismatch' }
1356
+ if ([string]::IsNullOrWhiteSpace([string]$policy.lease_file)) { Fail 'autonomous policy lease_file missing' }
1357
+ $leaseFile = [System.IO.Path]::GetFullPath([string]$policy.lease_file)
1358
+ $runtimeReal = [System.IO.Path]::GetFullPath($RuntimeDir).TrimEnd('\') + '\'
1359
+ if (-not $leaseFile.StartsWith($runtimeReal, [System.StringComparison]::OrdinalIgnoreCase)) { Fail 'autonomous policy lease_file must live under controller runtime directory' }
1360
+ return $leaseFile
1361
+ }
1362
+
1363
+ function Disarm-Autonomous($Cfg, [switch]$Quiet) {
1364
+ if ([string]::IsNullOrWhiteSpace([string]$Cfg.AutonomousPolicyFile)) { return }
1365
+ $leaseFile = Get-AutonomousRevocationLeaseFile $Cfg
1366
+ $epochError = $null
1367
+ if (Test-Path -LiteralPath $leaseFile -PathType Leaf) {
1368
+ try { Invoke-AuthorityEpochCommand $Cfg 'autonomous' 'user-revoke' $leaseFile | Out-Null } catch { $epochError = $_.Exception }
1369
+ Remove-Item -LiteralPath $leaseFile -Force -ErrorAction SilentlyContinue
1370
+ }
1371
+ if (Test-Path -LiteralPath $leaseFile) { throw 'Autonomous authority revocation failed: grant remains present' }
1372
+ if ($null -ne $epochError) { throw $epochError }
1373
+ if (-not $Quiet) { Write-Host 'AUTONOMOUS PLANE DISARMED' }
1374
+ }
1375
+
1376
+ function Show-AutonomousStatus($Cfg) {
1377
+ $a = Get-AutonomousLeaseStatus $Cfg
1378
+ Write-Host ("Autonomous : {0} [{1}]" -f $(if ($a.Armed) { 'ARMED' } elseif ($a.Configured) { 'DISARMED' } else { 'UNCONFIGURED' }), $a.Reason)
1379
+ return $a
1380
+ }
1381
+
1382
+ function Get-DesktopPolicyMetadata($Cfg) {
1383
+ if ([string]::IsNullOrWhiteSpace([string]$Cfg.DesktopPolicyFile)) { Fail 'desktop plane is not configured' }
1384
+ $policyPath = [string]$Cfg.DesktopPolicyFile
1385
+ try { $policy = Get-Content -LiteralPath $policyPath -Raw -Encoding UTF8 | ConvertFrom-Json } catch { Fail "invalid desktop policy JSON: $policyPath" }
1386
+ if ($policy.schema -ne 'deadbyte.desktop-policy.v1' -or $policy.enabled -ne $true) { Fail 'desktop policy schema/enabled mismatch' }
1387
+ if ([string]::IsNullOrWhiteSpace([string]$policy.grant_file)) { Fail 'desktop policy grant_file missing' }
1388
+ if ([string]::IsNullOrWhiteSpace([string]$policy.evidence_dir)) { Fail 'desktop policy evidence_dir missing' }
1389
+ $grantFile = [System.IO.Path]::GetFullPath([string]$policy.grant_file)
1390
+ $evidenceDir = [System.IO.Path]::GetFullPath([string]$policy.evidence_dir)
1391
+ $runtimeReal = [System.IO.Path]::GetFullPath($RuntimeDir).TrimEnd('\') + '\'
1392
+ if (-not $grantFile.StartsWith($runtimeReal, [System.StringComparison]::OrdinalIgnoreCase)) { Fail 'desktop policy grant_file must live under controller runtime directory' }
1393
+ if (-not ($evidenceDir.TrimEnd('\') + '\').StartsWith($runtimeReal, [System.StringComparison]::OrdinalIgnoreCase)) { Fail 'desktop policy evidence_dir must live under controller runtime directory' }
1394
+ $knownActions = @('pointer_move','click_primary','click_secondary','double_click_primary','scroll','key_chord','text_input')
1395
+ $actionKinds = @($policy.allowed_actions)
1396
+ $uniqueActions = @($actionKinds | Sort-Object -Unique)
1397
+ if ($actionKinds.Count -lt 1 -or $actionKinds.Count -ne $uniqueActions.Count -or @($actionKinds | Where-Object { $_ -notin $knownActions }).Count -gt 0) { Fail 'desktop policy allowed_actions invalid' }
1398
+ if (-not $policy.observer -or [string]::IsNullOrWhiteSpace([string]$policy.observer.executable) -or [string]$policy.observer.sha256 -cnotmatch '^[0-9a-f]{64}$') { Fail 'desktop observer metadata invalid' }
1399
+ if (-not $policy.input -or [string]::IsNullOrWhiteSpace([string]$policy.input.executable) -or [string]$policy.input.sha256 -cnotmatch '^[0-9a-f]{64}$') { Fail 'desktop input metadata invalid' }
1400
+ if (-not $policy.grounder -or [string]::IsNullOrWhiteSpace([string]$policy.grounder.executable) -or [string]$policy.grounder.sha256 -cnotmatch '^[0-9a-f]{64}$') { Fail 'desktop grounder metadata invalid' }
1401
+ $observerPath = [System.IO.Path]::GetFullPath([string]$policy.observer.executable)
1402
+ $inputPath = [System.IO.Path]::GetFullPath([string]$policy.input.executable)
1403
+ $grounderPath = [System.IO.Path]::GetFullPath([string]$policy.grounder.executable)
1404
+ foreach ($provider in @(
1405
+ [pscustomobject]@{ Name='observer'; Path=$observerPath; Sha256=([string]$policy.observer.sha256).ToLowerInvariant() },
1406
+ [pscustomobject]@{ Name='input'; Path=$inputPath; Sha256=([string]$policy.input.sha256).ToLowerInvariant() },
1407
+ [pscustomobject]@{ Name='grounder'; Path=$grounderPath; Sha256=([string]$policy.grounder.sha256).ToLowerInvariant() }
1408
+ )) {
1409
+ if (-not (Test-Path -LiteralPath $provider.Path -PathType Leaf)) { Fail "desktop $($provider.Name) executable missing: $($provider.Path)" }
1410
+ $item = Get-Item -LiteralPath $provider.Path -Force
1411
+ if (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { Fail "desktop $($provider.Name) executable must not be a reparse point" }
1412
+ $actual = Get-ExecutableHash $provider.Path
1413
+ if ($actual -ne $provider.Sha256) { Fail "desktop $($provider.Name) executable SHA-256 mismatch: expected $($provider.Sha256), found $actual" }
1414
+ }
1415
+ [pscustomobject]@{
1416
+ Path=$policyPath; Sha256=Get-ExecutableHash $policyPath; GrantFile=$grantFile; EvidenceDir=$evidenceDir;
1417
+ ActionKinds=$actionKinds; ObserverPath=$observerPath; ObserverSha256=([string]$policy.observer.sha256).ToLowerInvariant();
1418
+ InputPath=$inputPath; InputSha256=([string]$policy.input.sha256).ToLowerInvariant();
1419
+ GrounderPath=$grounderPath; GrounderSha256=([string]$policy.grounder.sha256).ToLowerInvariant()
1420
+ }
1421
+ }
1422
+
1423
+ function Get-DesktopGrantFileForRevoke($Cfg) {
1424
+ if ([string]::IsNullOrWhiteSpace([string]$Cfg.DesktopPolicyFile)) { return '' }
1425
+ $policyPath = [string]$Cfg.DesktopPolicyFile
1426
+ try { $policy = Get-Content -LiteralPath $policyPath -Raw -Encoding UTF8 | ConvertFrom-Json } catch { Fail "invalid desktop policy JSON during revoke: $policyPath" }
1427
+ if ([string]::IsNullOrWhiteSpace([string]$policy.grant_file)) { Fail 'desktop policy grant_file missing during revoke' }
1428
+ $grantFile = [System.IO.Path]::GetFullPath([string]$policy.grant_file)
1429
+ $runtimeReal = [System.IO.Path]::GetFullPath($RuntimeDir).TrimEnd('\') + '\'
1430
+ if (-not $grantFile.StartsWith($runtimeReal, [System.StringComparison]::OrdinalIgnoreCase)) { Fail 'desktop policy grant_file must live under controller runtime directory' }
1431
+ return $grantFile
1432
+ }
1433
+
1434
+ function Get-DesktopGrantStatus($Cfg) {
1435
+ if ([string]::IsNullOrWhiteSpace([string]$Cfg.DesktopPolicyFile)) { return [pscustomobject]@{ Configured=$false; Armed=$false; Reason='no-policy'; ActionKinds=@() } }
1436
+ try { $meta = Get-DesktopPolicyMetadata $Cfg } catch { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason=$_.Exception.Message; ActionKinds=@() } }
1437
+ if (-not (Test-Path -LiteralPath $meta.GrantFile -PathType Leaf)) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='disarmed'; ActionKinds=@() } }
1438
+ try { $grant = Get-Content -LiteralPath $meta.GrantFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='invalid-grant'; ActionKinds=@() } }
1439
+ if ($grant.schema -ne 'deadbyte.desktop-grant.v1') { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='grant-schema'; ActionKinds=@() } }
1440
+ if ([string]$grant.policy_sha256 -ne [string]$meta.Sha256) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='policy-digest'; ActionKinds=@() } }
1441
+ if ([string]$grant.instance_nonce -cnotmatch '^[0-9a-f]{64}$') { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='nonce-invalid'; ActionKinds=@() } }
1442
+ $actions = @($grant.action_kinds)
1443
+ $unique = @($actions | Sort-Object -Unique)
1444
+ if ($actions.Count -lt 1 -or $actions.Count -ne $unique.Count) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='actions-invalid'; ActionKinds=@() } }
1445
+ if (@($actions | Where-Object { $_ -notin $meta.ActionKinds }).Count -gt 0) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason='actions-outside-policy'; ActionKinds=@() } }
1446
+ $epoch = Get-AuthorityEpochStatus $Cfg 'desktop' $meta.GrantFile ([string]$grant.instance_nonce)
1447
+ if (-not $epoch.Trusted) { return [pscustomobject]@{ Configured=$true; Armed=$false; Reason=[string]$epoch.Reason; ActionKinds=$actions } }
1448
+ return [pscustomobject]@{ Configured=$true; Armed=$true; Reason='standing-grant'; ActionKinds=$actions; EpochSeq=$epoch.Seq; EpochSha256=$epoch.EventSha256 }
1449
+ }
1450
+
1451
+ function Arm-Desktop($Cfg) {
1452
+ $meta = Get-DesktopPolicyMetadata $Cfg
1453
+ Ensure-Directory $RuntimeDir
1454
+ Ensure-Directory $meta.EvidenceDir
1455
+ $nonceBytes = New-Object byte[] 32
1456
+ $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
1457
+ try { $rng.GetBytes($nonceBytes) } finally { $rng.Dispose() }
1458
+ $nonce = -join ($nonceBytes | ForEach-Object { $_.ToString('x2') })
1459
+ $body = [ordered]@{
1460
+ schema = 'deadbyte.desktop-grant.v1'
1461
+ policy_sha256 = [string]$meta.Sha256
1462
+ instance_nonce = $nonce
1463
+ action_kinds = @($meta.ActionKinds)
1464
+ granted_at_utc = [DateTime]::UtcNow.ToString('o')
1465
+ }
1466
+ $tmp = $meta.GrantFile + '.tmp'
1467
+ $body | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $tmp -Encoding UTF8
1468
+ Move-Item -LiteralPath $tmp -Destination $meta.GrantFile -Force
1469
+ try { Invoke-AuthorityEpochCommand $Cfg 'desktop' 'grant' $meta.GrantFile | Out-Null }
1470
+ catch {
1471
+ Remove-Item -LiteralPath $meta.GrantFile -Force -ErrorAction SilentlyContinue
1472
+ throw
1473
+ }
1474
+ Write-Host ("DESKTOP PLANE ENABLED UNTIL EXPLICIT REVOKE [{0}]" -f ($meta.ActionKinds -join ','))
1475
+ }
1476
+
1477
+ function Disarm-Desktop($Cfg, [switch]$Quiet) {
1478
+ $grantFile = Get-DesktopGrantFileForRevoke $Cfg
1479
+ if ([string]::IsNullOrWhiteSpace([string]$grantFile)) { return }
1480
+ $epochError = $null
1481
+ if (Test-Path -LiteralPath $grantFile -PathType Leaf) {
1482
+ try { Invoke-AuthorityEpochCommand $Cfg 'desktop' 'user-revoke' $grantFile | Out-Null } catch { $epochError = $_.Exception }
1483
+ Remove-Item -LiteralPath $grantFile -Force -ErrorAction SilentlyContinue
1484
+ }
1485
+ if (Test-Path -LiteralPath $grantFile) { throw 'Desktop authority revocation failed: grant remains present' }
1486
+ if ($null -ne $epochError) { throw $epochError }
1487
+ if (-not $Quiet) { Write-Host 'DESKTOP PLANE DISARMED' }
1488
+ }
1489
+
1490
+ function Show-DesktopStatus($Cfg) {
1491
+ $d = Get-DesktopGrantStatus $Cfg
1492
+ Write-Host ("Desktop plane : {0} [{1}]" -f $(if ($d.Armed) { 'ARMED' } elseif ($d.Configured) { 'DISARMED' } else { 'UNCONFIGURED' }), $d.Reason)
1493
+ return $d
1494
+ }
1495
+
1496
+ function Get-AutonomousSupervisorStatus($Cfg) {
1497
+ if (-not [bool]$Cfg.AutonomousSupervisorEnabled) { return [pscustomobject]@{ Configured=$false; Running=$false; Reason='disabled'; Owned=$null } }
1498
+ $owned = Test-OwnedInstance 'Supervisor'
1499
+ [pscustomobject]@{ Configured=$true; Running=[bool]$owned.Alive; Reason=[string]$owned.Reason; Owned=$owned }
1500
+ }
1501
+
1502
+ function Start-AutonomousSupervisor($Cfg) {
1503
+ if (-not [bool]$Cfg.AutonomousSupervisorEnabled) { Fail 'autonomous supervisor is disabled by trusted config' }
1504
+ if (-not [bool]$Cfg.AutonomousRuntimeEnabled) { Fail 'autonomous supervisor requires AutonomousRuntimeEnabled' }
1505
+ Start-OwnedComponent 'Supervisor' $Cfg.Supervisor $Cfg | Out-Null
1506
+ $status = Get-AutonomousSupervisorStatus $Cfg
1507
+ if (-not $status.Running) { Fail "autonomous supervisor start failed: $($status.Reason)" }
1508
+ return $status
1509
+ }
1510
+
1511
+ function Stop-AutonomousSupervisor($Cfg) {
1512
+ if (-not [bool]$Cfg.AutonomousSupervisorEnabled) { return Get-AutonomousSupervisorStatus $Cfg }
1513
+ Stop-OwnedComponent 'Supervisor'
1514
+ return Get-AutonomousSupervisorStatus $Cfg
1515
+ }
1516
+
1517
+ function Show-AutonomousSupervisorStatus($Cfg) {
1518
+ $s = Get-AutonomousSupervisorStatus $Cfg
1519
+ $label = if (-not $s.Configured) { 'DISABLED' } elseif ($s.Running) { 'RUNNING' } else { 'STOPPED' }
1520
+ Write-Host ("Loop supervisor: {0} [{1}]" -f $label, $s.Reason)
1521
+ return $s
1522
+ }
1523
+
1524
+ function Recover-AutonomousSupervisor($Cfg) {
1525
+ if (-not [bool]$Cfg.AutonomousSupervisorEnabled) { Fail 'autonomous supervisor recovery requires AutonomousSupervisorEnabled' }
1526
+ Disarm-Desktop $Cfg -Quiet
1527
+ Start-Deadbyte $Cfg | Out-Null
1528
+ try { Disarm-Autonomous $Cfg -Quiet } catch {}
1529
+ try { Disarm-Coding $Cfg } catch {}
1530
+ try { Disarm-Process $Cfg } catch {}
1531
+ try { Disarm-Host $Cfg } catch {}
1532
+ $runtime = Get-ObservedState $Cfg
1533
+ if ($runtime.State -ne 'ONLINE') { Fail "supervisor recovery requires ONLINE runtime; observed $($runtime.State)" }
1534
+ $process = Get-ProcessGrantStatus $Cfg
1535
+ $coding = Get-CodingLeaseStatus $Cfg
1536
+ $autonomous = Get-AutonomousLeaseStatus $Cfg
1537
+ $desktop = Get-DesktopGrantStatus $Cfg
1538
+ $hostStatus = Get-HostArmStatus $Cfg
1539
+ if ($process.Armed -or $coding.Armed -or $autonomous.Armed -or $desktop.Armed -or $hostStatus.Armed) { Fail 'supervisor recovery final authority is not fully disarmed' }
1540
+ $supervisor = Start-AutonomousSupervisor $Cfg
1541
+ Write-Host 'AUTONOMOUS SUPERVISOR RECOVERY PASS: runtime ONLINE; privileged planes DISARMED.'
1542
+ return [pscustomobject]@{ Runtime=$runtime; Supervisor=$supervisor; Process=$process; Coding=$coding; Autonomous=$autonomous; Desktop=$desktop; Host=$hostStatus }
1543
+ }
1544
+
1545
+ function Get-BoundedObservationReason($Value) {
1546
+ $reason = [string]$Value
1547
+ if ($reason -cmatch '^[a-z][a-z0-9-]{0,63}$') { return $reason }
1548
+ return 'unverified'
1549
+ }
1550
+
1551
+ function Convert-OwnedObservation($Owned) {
1552
+ $pidValue = $null; $birth = $null
1553
+ if ($Owned.Alive -and $Owned.Ledger) {
1554
+ $pidValue = [int]$Owned.Ledger.pid
1555
+ $birth = [string]$Owned.Ledger.creation_time_utc
1556
+ }
1557
+ [ordered]@{ alive=[bool]$Owned.Alive; reason=(Get-BoundedObservationReason $Owned.Reason); pid=$pidValue; creation_time_utc=$birth }
1558
+ }
1559
+
1560
+ function Convert-AuthorityObservation($Authority) {
1561
+ $out = [ordered]@{ configured=[bool]$Authority.Configured; armed=[bool]$Authority.Armed; reason=(Get-BoundedObservationReason $Authority.Reason) }
1562
+ $seqProp = $Authority.PSObject.Properties['EpochSeq']
1563
+ $shaProp = $Authority.PSObject.Properties['EpochSha256']
1564
+ if ($null -ne $seqProp -or $null -ne $shaProp) {
1565
+ if ($null -eq $seqProp -or $null -eq $shaProp -or [int64]$seqProp.Value -lt 1 -or [string]$shaProp.Value -cnotmatch '^[0-9a-f]{64}$') {
1566
+ Fail 'authority epoch observation incomplete or invalid'
1567
+ }
1568
+ $out['epoch_seq'] = [int64]$seqProp.Value
1569
+ $out['epoch_sha256'] = [string]$shaProp.Value
1570
+ }
1571
+ return $out
1572
+ }
1573
+
1574
+ function Get-DownstreamCoreObservation($Cfg, $Bridge) {
1575
+ $out = [ordered]@{ alive=$false; reason='bridge-unavailable'; pid=$null; parent_pid=$null; creation_time_utc=$null; identity_basis=$null }
1576
+ if (-not $Bridge.Alive) { return $out }
1577
+ $bridgePid = [int]$Bridge.Ledger.pid
1578
+ if (-not [string]::Equals([IO.Path]::GetFullPath([string]$Bridge.Ledger.script_path), [IO.Path]::GetFullPath([string]$Cfg.Bridge.ScriptPath), [StringComparison]::OrdinalIgnoreCase) -or [string]$Bridge.Ledger.script_sha256 -ne [string]$Cfg.Bridge.ScriptSha256) {
1579
+ $out.reason='bridge-config-mismatch'; return $out
1580
+ }
1581
+ try {
1582
+ $children = @(Get-CimInstance Win32_Process -Filter ("ParentProcessId={0} AND Name='node.exe'" -f $bridgePid) -ErrorAction Stop)
1583
+ if ($children.Count -eq 0) { $out.reason='startup-zero-child'; return $out }
1584
+ if ($children.Count -ne 1) { $out.reason='ambiguous-core'; return $out }
1585
+ $child = $children[0]
1586
+ if ([int]$child.ParentProcessId -ne $bridgePid -or [int]$child.ProcessId -lt 1 -or $null -eq $child.CreationDate) { $out.reason='core-identity-incomplete'; return $out }
1587
+ if (-not [string]::IsNullOrWhiteSpace([string]$child.ExecutablePath) -and -not [string]::Equals([string]$child.ExecutablePath,[string]$Cfg.Bridge.FilePath,[StringComparison]::OrdinalIgnoreCase)) { $out.reason='core-image-mismatch'; return $out }
1588
+ $command = [string]$child.CommandLine
1589
+ $needle = (Join-Path ([string]$Cfg.CoreRoot) 'src\mcp-server.mjs').Replace('/','\').ToLowerInvariant()
1590
+ if ([string]::IsNullOrWhiteSpace($command)) { $basis='verified-bridge-single-opaque-node-child' }
1591
+ elseif ($command.Replace('/','\').ToLowerInvariant().Contains($needle)) { $basis='command-line' }
1592
+ else { $out.reason='wrong-core-generation'; return $out }
1593
+ $out.alive=$true; $out.reason='verified'; $out.pid=[int]$child.ProcessId; $out.parent_pid=$bridgePid
1594
+ $out.creation_time_utc=$child.CreationDate.ToUniversalTime().ToString('o'); $out.identity_basis=$basis
1595
+ return $out
1596
+ } catch { $out.reason='core-observation-failed'; return $out }
1597
+ }
1598
+
1599
+ function Get-StructuredObservation($Cfg) {
1600
+ $s = Get-ObservedState $Cfg
1601
+ $supervisor = Get-AutonomousSupervisorStatus $Cfg
1602
+ [ordered]@{
1603
+ schema='deadbyte.controller-observation.v1'
1604
+ observed_at_utc=[DateTime]::UtcNow.ToString('o')
1605
+ state=[string]$s.State
1606
+ core_root=[string]$Cfg.CoreRoot
1607
+ local_ready=[bool]$s.LocalReady
1608
+ bridge=(Convert-OwnedObservation $s.Bridge)
1609
+ core=$s.Core
1610
+ cloud=(Convert-OwnedObservation $s.Cloud)
1611
+ supervisor=[ordered]@{ configured=[bool]$supervisor.Configured; running=[bool]$supervisor.Running; reason=(Get-BoundedObservationReason $supervisor.Reason) }
1612
+ authority=[ordered]@{
1613
+ process=(Convert-AuthorityObservation (Get-ProcessGrantStatus $Cfg))
1614
+ coding=(Convert-AuthorityObservation (Get-CodingLeaseStatus $Cfg))
1615
+ autonomous=(Convert-AuthorityObservation (Get-AutonomousLeaseStatus $Cfg))
1616
+ desktop=(Convert-AuthorityObservation (Get-DesktopGrantStatus $Cfg))
1617
+ host=(Convert-AuthorityObservation (Get-HostArmStatus $Cfg))
1618
+ }
1619
+ }
1620
+ }
1621
+
1622
+ function Show-State($Cfg) {
1623
+ $s = Get-ObservedState $Cfg
1624
+ $bp = if ($s.Bridge.Alive) { $s.Bridge.Ledger.pid } else { '-' }
1625
+ $cp = if ($s.Cloud.Alive) { $s.Cloud.Ledger.pid } else { '-' }
1626
+ $url = Get-CurrentUrl $Cfg
1627
+ Write-Host ("DEADBYTE MCP = {0}" -f $s.State)
1628
+ Write-Host ("Bridge PID : {0} [{1}]" -f $bp, $s.Bridge.Reason)
1629
+ Write-Host ("Core PID : {0} [{1}]" -f $(if ($s.Core.alive) { $s.Core.pid } else { '-' }), $s.Core.reason)
1630
+ Write-Host ("Cloud PID : {0} [{1}]" -f $cp, $s.Cloud.Reason)
1631
+ Write-Host ("Local ready : {0}" -f $s.LocalReady)
1632
+ Write-Host ("URL : {0}" -f $(if ($url) { $url } else { '<unknown>' }))
1633
+ Write-Host ("Machine runtime: {0}" -f $(if ([bool]$Cfg.MachineRuntimeEnabled) { 'ENABLED' } else { 'DISABLED' }))
1634
+ Write-Host ("Agent runtime : {0}" -f $(if ([bool]$Cfg.AgentRuntimeEnabled) { 'ENABLED' } else { 'DISABLED' }))
1635
+ Write-Host ("Process rt : {0}" -f $(if ([bool]$Cfg.ProcessRuntimeEnabled) { 'ENABLED' } else { 'DISABLED' }))
1636
+ Write-Host ("Coding runtime: {0}" -f $(if ([bool]$Cfg.CodingRuntimeEnabled) { 'ENABLED' } else { 'DISABLED' }))
1637
+ Write-Host ("Machine FS rt : {0}" -f $(if ([bool]$Cfg.MachineFsRuntimeEnabled) { 'ENABLED' } else { 'DISABLED' }))
1638
+ Write-Host ("Observation rt: {0}" -f $(if ([bool]$Cfg.ObservationRuntimeEnabled) { 'ENABLED' } else { 'DISABLED' }))
1639
+ Write-Host ("Desktop runtime: {0}" -f $(if ([bool]$Cfg.DesktopRuntimeEnabled) { 'ENABLED' } else { 'DISABLED' }))
1640
+ Write-Host ("Autonomous rt : {0}" -f $(if ([bool]$Cfg.AutonomousRuntimeEnabled) { 'ENABLED' } else { 'DISABLED' }))
1641
+ Show-AutonomousSupervisorStatus $Cfg | Out-Null
1642
+ Show-ProcessStatus $Cfg | Out-Null
1643
+ Show-CodingStatus $Cfg | Out-Null
1644
+ Show-AutonomousStatus $Cfg | Out-Null
1645
+ Show-DesktopStatus $Cfg | Out-Null
1646
+ Show-HostStatus $Cfg | Out-Null
1647
+ return $s
1648
+ }
1649
+
1650
+ function Invoke-MachineSmoke($Cfg) {
1651
+ if (-not [bool]$Cfg.MachineRuntimeEnabled) { Fail 'machinetest requires MachineRuntimeEnabled' }
1652
+ $s = Get-ObservedState $Cfg
1653
+ if ($s.State -ne 'ONLINE') { Fail "machinetest requires ONLINE; observed $($s.State)" }
1654
+ $url = Get-CurrentUrl $Cfg
1655
+ if (-not $url) { Fail 'public MCP URL unavailable' }
1656
+ Wait-PublicDnsReady $Cfg $url
1657
+ $probeArgs = @($url)
1658
+ if (-not [string]::IsNullOrWhiteSpace([string]$env:DEADBYTE_REMOTE_PROBE_MODE)) {
1659
+ if ([string]$env:DEADBYTE_REMOTE_PROBE_MODE -notin @('default','operator','desktop','host','full','revoked')) { Fail 'invalid remote enforcement probe mode' }
1660
+ $probeArgs += [string]$env:DEADBYTE_REMOTE_PROBE_MODE
1661
+ }
1662
+ Invoke-CoreGate $Cfg 'remote-mcp-machine-smoke-client.mjs' $probeArgs
1663
+ Write-Host 'MACHINE IDENTITY LIVE GATE PASS: public status/list + SPKI identity + signed Ed25519 ping verified.'
1664
+ }
1665
+
1666
+ function Invoke-Smoke($Cfg, [bool]$Full) {
1667
+ $s = Get-ObservedState $Cfg
1668
+ if ($s.State -ne 'ONLINE') { Fail "MCP test requires ONLINE; observed $($s.State)" }
1669
+ if (-not (Test-LocalReady $Cfg)) { Fail 'local readiness probe failed' }
1670
+ $url = Get-CurrentUrl $Cfg
1671
+ if (-not $url) { Fail 'public MCP URL unavailable; configure McpUrlFile or McpPathFile' }
1672
+ Wait-PublicDnsReady $Cfg $url
1673
+ Invoke-CoreGate $Cfg 'remote-mcp-smoke-client.mjs' @($url, $(if ($Full) { 'full' } else { 'smoke' }))
1674
+ if ($Full) {
1675
+ Invoke-CoreGate $Cfg 'mcp-contained-smoke-client.mjs'
1676
+ Write-Host 'FULL GATE PASS: public modern MCP + proof + public contained proof + local canonical 3-operation AppContainer/replay/evidence gate.'
1677
+ } else {
1678
+ Write-Host 'MCP SMOKE PASS: public modern MCP + exact tool catalog + signed proof + independent verification.'
1679
+ }
1680
+ }
1681
+
1682
+ function Invoke-HostSmoke($Cfg) {
1683
+ $h = Get-HostArmStatus $Cfg
1684
+ if (-not $h.Armed) { Fail 'hosttest requires an active local arm lease; run armhost first' }
1685
+ $s = Get-ObservedState $Cfg
1686
+ if ($s.State -ne 'ONLINE') { Fail "hosttest requires ONLINE; observed $($s.State)" }
1687
+ $url = Get-CurrentUrl $Cfg
1688
+ if (-not $url) { Fail 'public MCP URL unavailable' }
1689
+ Wait-PublicDnsReady $Cfg $url
1690
+ Invoke-CoreGate $Cfg 'remote-mcp-host-smoke-client.mjs' @($url)
1691
+ Write-Host 'HOST PLANE LIVE GATE PASS: public host write + hash readback + hash-pinned exec + cleanup.'
1692
+ }
1693
+
1694
+ function Invoke-CodingSmoke($Cfg) {
1695
+ $c = Get-CodingLeaseStatus $Cfg
1696
+ if (-not $c.Armed) { Fail 'codingtest requires an active local coding lease; run armcoding first' }
1697
+ $s = Get-ObservedState $Cfg
1698
+ if ($s.State -ne 'ONLINE') { Fail "codingtest requires ONLINE; observed $($s.State)" }
1699
+ $url = Get-CurrentUrl $Cfg
1700
+ if (-not $url) { Fail 'public MCP URL unavailable' }
1701
+ Wait-PublicDnsReady $Cfg $url
1702
+ Invoke-CoreGate $Cfg 'remote-mcp-coding-smoke-client.mjs' @($url)
1703
+ Write-Host 'CODING PLANE LIVE GATE PASS: public edit + command + benchmark + durable coding-task receipt verification.'
1704
+ }
1705
+
1706
+ function Invoke-AutonomousSmoke($Cfg) {
1707
+ $a = Get-AutonomousLeaseStatus $Cfg
1708
+ if (-not $a.Armed) { Fail 'autonomoustest requires active coding + autonomous leases; run armcoding then armautonomous' }
1709
+ $s = Get-ObservedState $Cfg
1710
+ if ($s.State -ne 'ONLINE') { Fail "autonomoustest requires ONLINE; observed $($s.State)" }
1711
+ $url = Get-CurrentUrl $Cfg
1712
+ if (-not $url) { Fail 'public MCP URL unavailable' }
1713
+ Wait-PublicDnsReady $Cfg $url
1714
+ Invoke-CoreGate $Cfg 'remote-mcp-autonomous-smoke-client.mjs' @($url)
1715
+ Write-Host 'AUTONOMOUS PRODUCTION LIVE GATE PASS: public sampling MRTR + native diff + fail/diagnose/replan + build/test/benchmark + signed release gate.'
1716
+ }
1717
+
1718
+ function Clean-Stale {
1719
+ Ensure-Directory $RuntimeDir
1720
+ foreach ($name in @('Bridge','Cloud','Supervisor')) {
1721
+ $o = Test-OwnedInstance $name
1722
+ if ($o.Ledger -and -not $o.Alive -and $o.Reason -eq 'process-missing') {
1723
+ Remove-Item -LiteralPath (Ledger-Path $name) -Force -ErrorAction SilentlyContinue
1724
+ } elseif ($o.Ledger -and -not $o.Alive -and $o.Reason -ne 'no-ledger') {
1725
+ Write-Warning "$name ledger retained: $($o.Reason). No unverified process was killed."
1726
+ }
1727
+ }
1728
+ }
1729
+
1730
+ function Run-CommandCore([string]$Name) {
1731
+ $cfg = Get-Config
1732
+ Assert-ControllerRootBound $cfg
1733
+ $readOnly = $Name -in @('status','url','hoststatus','processstatus','codingstatus','autonomousstatus','desktopstatus','supervisorstatus')
1734
+ if (-not $readOnly) { Ensure-Directory $RuntimeDir; Ensure-Directory $LogDir }
1735
+ switch ($Name) {
1736
+ 'start' { Start-Deadbyte $cfg | Out-Null; if ([bool]$cfg.AutonomousSupervisorEnabled) { Start-AutonomousSupervisor $cfg | Out-Null }; Show-State $cfg | Out-Null }
1737
+ 'resume' { Start-Deadbyte $cfg | Out-Null; if ([bool]$cfg.AutonomousSupervisorEnabled) { Start-AutonomousSupervisor $cfg | Out-Null }; Show-State $cfg | Out-Null }
1738
+ 'park' { Park-Deadbyte $cfg | Out-Null; Show-State $cfg | Out-Null }
1739
+ 'stop' { if ([bool]$cfg.AutonomousSupervisorEnabled) { Stop-AutonomousSupervisor $cfg | Out-Null }; Stop-Deadbyte $cfg | Out-Null; Show-State $cfg | Out-Null }
1740
+ 'restart' { if ([bool]$cfg.AutonomousSupervisorEnabled) { Stop-AutonomousSupervisor $cfg | Out-Null }; Stop-Deadbyte $cfg | Out-Null; Start-Deadbyte $cfg | Out-Null; if ([bool]$cfg.AutonomousSupervisorEnabled) { Start-AutonomousSupervisor $cfg | Out-Null }; Show-State $cfg | Out-Null }
1741
+ 'status' { if ($Json) { Get-StructuredObservation $cfg | ConvertTo-Json -Depth 6 -Compress | Write-Output } else { Show-State $cfg | Out-Null } }
1742
+ 'url' { $u=Get-CurrentUrl $cfg; if (-not $u) { Fail 'URL unavailable' }; Write-Output $u }
1743
+ 'test' { Invoke-Smoke $cfg $false }
1744
+ 'fulltest'{ Invoke-Smoke $cfg $true }
1745
+ 'toggle' { $s=Get-ObservedState $cfg; if ($s.State -eq 'ONLINE') { Park-Deadbyte $cfg | Out-Null } elseif ($s.State -in @('PARKED','OFFLINE')) { Start-Deadbyte $cfg | Out-Null } else { Fail 'toggle refuses DEGRADED state' }; Show-State $cfg | Out-Null }
1746
+ 'clean' { Clean-Stale; Show-State $cfg | Out-Null }
1747
+ 'machinetest' { Invoke-MachineSmoke $cfg }
1748
+ 'armhost' { Arm-Host $cfg; Show-HostStatus $cfg | Out-Null }
1749
+ 'disarmhost' { Disarm-Host $cfg; Show-HostStatus $cfg | Out-Null }
1750
+ 'hoststatus' { Show-HostStatus $cfg | Out-Null }
1751
+ 'hosttest' { Invoke-HostSmoke $cfg }
1752
+ 'armprocess' { Arm-Process $cfg; Show-ProcessStatus $cfg | Out-Null }
1753
+ 'disarmprocess' { Disarm-Process $cfg; Show-ProcessStatus $cfg | Out-Null }
1754
+ 'processstatus' { Show-ProcessStatus $cfg | Out-Null }
1755
+ 'armcoding' { Arm-Coding $cfg; Show-CodingStatus $cfg | Out-Null }
1756
+ 'disarmcoding' { Disarm-Coding $cfg; Show-CodingStatus $cfg | Out-Null; Show-AutonomousStatus $cfg | Out-Null }
1757
+ 'codingstatus' { Show-CodingStatus $cfg | Out-Null }
1758
+ 'codingtest' { Invoke-CodingSmoke $cfg }
1759
+ 'armautonomous' { Arm-Autonomous $cfg; Show-AutonomousStatus $cfg | Out-Null }
1760
+ 'disarmautonomous' { Disarm-Autonomous $cfg; Show-AutonomousStatus $cfg | Out-Null }
1761
+ 'autonomousstatus' { Show-AutonomousStatus $cfg | Out-Null }
1762
+ 'autonomoustest' { Invoke-AutonomousSmoke $cfg }
1763
+ 'armdesktop' { Arm-Desktop $cfg; Show-DesktopStatus $cfg | Out-Null }
1764
+ 'disarmdesktop' { Disarm-Desktop $cfg; Show-DesktopStatus $cfg | Out-Null }
1765
+ 'desktopstatus' { Show-DesktopStatus $cfg | Out-Null }
1766
+ 'supervisorstart' { Start-AutonomousSupervisor $cfg | Out-Null; Show-AutonomousSupervisorStatus $cfg | Out-Null }
1767
+ 'supervisorstop' { Stop-AutonomousSupervisor $cfg | Out-Null; Show-AutonomousSupervisorStatus $cfg | Out-Null }
1768
+ 'supervisorstatus' { Show-AutonomousSupervisorStatus $cfg | Out-Null }
1769
+ 'supervisorrecover' { Recover-AutonomousSupervisor $cfg | Out-Null; Show-State $cfg | Out-Null }
1770
+ default { Fail "unknown command '$Name'" }
1771
+ }
1772
+ }
1773
+
1774
+ function Run-Command([string]$Name) {
1775
+ $mutating = $Name -in @(
1776
+ 'start','resume','park','stop','restart','toggle','clean',
1777
+ 'armhost','disarmhost','armprocess','disarmprocess','armcoding','disarmcoding',
1778
+ 'armautonomous','disarmautonomous','armdesktop','disarmdesktop',
1779
+ 'supervisorstart','supervisorstop','supervisorrecover'
1780
+ )
1781
+ if (-not $mutating) {
1782
+ Run-CommandCore $Name
1783
+ return
1784
+ }
1785
+ Invoke-WithControllerMutationLock $Name { Run-CommandCore $Name }
1786
+ }
1787
+
1788
+ if ($Command -ne 'menu') {
1789
+ $recordsStartFailure = $Command -in @('start','resume','restart')
1790
+ if ($recordsStartFailure) { Remove-Item -LiteralPath (Controller-StartFailurePath) -Force -ErrorAction SilentlyContinue }
1791
+ try {
1792
+ Run-Command $Command
1793
+ exit 0
1794
+ } catch {
1795
+ if ($recordsStartFailure) {
1796
+ try { Write-ControllerStartFailure $Command $_.Exception } catch {}
1797
+ }
1798
+ throw
1799
+ }
1800
+ }
1801
+
1802
+ while ($true) {
1803
+ Clear-Host
1804
+ $cfg = Get-Config
1805
+ Assert-ControllerRootBound $cfg
1806
+ Write-Host 'DEADBYTE MCP ON-DEMAND CONTROLLER V0.15.5 / R46'
1807
+ Write-Host '======================================'
1808
+ Show-State $cfg | Out-Null
1809
+ Write-Host ''
1810
+ Write-Host '[1] Start / Resume'
1811
+ Write-Host '[2] Park'
1812
+ Write-Host '[3] Full Stop'
1813
+ Write-Host '[4] Restart'
1814
+ Write-Host '[5] Transport smoke test'
1815
+ Write-Host '[6] Public transport gate'
1816
+ Write-Host '[7] Show current URL'
1817
+ Write-Host '[8] Clean stale ledgers'
1818
+ Write-Host '[9] Enable privileged host plane (standing grant)'
1819
+ Write-Host '[10] Revoke privileged host plane'
1820
+ Write-Host '[11] Host plane status'
1821
+ Write-Host '[12] Host plane LIVE gate (requires grant)'
1822
+ Write-Host '[13] Enable coding plane (standing grant)'
1823
+ Write-Host '[14] Revoke coding plane'
1824
+ Write-Host '[15] Coding plane status'
1825
+ Write-Host '[16] Coding plane LIVE gate (requires grant)'
1826
+ Write-Host '[17] Enable autonomous plane (standing grant)'
1827
+ Write-Host '[18] Revoke autonomous plane'
1828
+ Write-Host '[19] Autonomous plane status'
1829
+ Write-Host '[20] Autonomous production LIVE gate'
1830
+ Write-Host '[21] Machine identity LIVE gate (non-mutating)'
1831
+ Write-Host '[22] Enable process plane (standing grant)'
1832
+ Write-Host '[23] Revoke process plane'
1833
+ Write-Host '[24] Process plane status'
1834
+ Write-Host '[25] Enable desktop plane (standing grant; never auto-armed)'
1835
+ Write-Host '[26] Revoke desktop plane'
1836
+ Write-Host '[27] Desktop plane status'
1837
+ Write-Host '[0] Exit controller (runtime stays as-is)'
1838
+ $choice = Read-Host 'Select'
1839
+ try {
1840
+ switch ($choice) {
1841
+ '1' { Run-Command 'start' }
1842
+ '2' { Run-Command 'park' }
1843
+ '3' { Run-Command 'stop' }
1844
+ '4' { Run-Command 'restart' }
1845
+ '5' { Run-Command 'test' }
1846
+ '6' { Run-Command 'fulltest' }
1847
+ '7' { Run-Command 'url' }
1848
+ '8' { Run-Command 'clean' }
1849
+ '9' { Run-Command 'armhost' }
1850
+ '10' { Run-Command 'disarmhost' }
1851
+ '11' { Run-Command 'hoststatus' }
1852
+ '12' { Run-Command 'hosttest' }
1853
+ '13' { Run-Command 'armcoding' }
1854
+ '14' { Run-Command 'disarmcoding' }
1855
+ '15' { Run-Command 'codingstatus' }
1856
+ '16' { Run-Command 'codingtest' }
1857
+ '17' { Run-Command 'armautonomous' }
1858
+ '18' { Run-Command 'disarmautonomous' }
1859
+ '19' { Run-Command 'autonomousstatus' }
1860
+ '20' { Run-Command 'autonomoustest' }
1861
+ '21' { Run-Command 'machinetest' }
1862
+ '22' { Run-Command 'armprocess' }
1863
+ '23' { Run-Command 'disarmprocess' }
1864
+ '24' { Run-Command 'processstatus' }
1865
+ '25' { Run-Command 'armdesktop' }
1866
+ '26' { Run-Command 'disarmdesktop' }
1867
+ '27' { Run-Command 'desktopstatus' }
1868
+ '0' { exit 0 }
1869
+ default { Write-Warning 'invalid selection' }
1870
+ }
1871
+ } catch { Write-Host $_.Exception.Message -ForegroundColor Red }
1872
+ if ($choice -ne '0') { [void](Read-Host 'Press Enter') }
1873
+ }