patchwarden 0.4.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PatchWarden.cmd +51 -0
- package/README.en.md +1022 -0
- package/README.md +897 -358
- package/dist/assessments/agentAssessor.d.ts +15 -0
- package/dist/assessments/agentAssessor.js +293 -0
- package/dist/assessments/assessmentStore.d.ts +133 -0
- package/dist/assessments/assessmentStore.js +238 -0
- package/dist/assessments/confirmCli.d.ts +9 -0
- package/dist/assessments/confirmCli.js +26 -0
- package/dist/config.d.ts +22 -1
- package/dist/config.js +156 -3
- package/dist/direct/directAudit.d.ts +23 -0
- package/dist/direct/directAudit.js +309 -0
- package/dist/direct/directGuards.d.ts +20 -0
- package/dist/direct/directGuards.js +137 -0
- package/dist/direct/directPatch.d.ts +17 -0
- package/dist/direct/directPatch.js +113 -0
- package/dist/direct/directSessionStore.d.ts +63 -0
- package/dist/direct/directSessionStore.js +192 -0
- package/dist/direct/directVerification.d.ts +12 -0
- package/dist/direct/directVerification.js +96 -0
- package/dist/doctor.js +73 -5
- package/dist/logging.d.ts +52 -0
- package/dist/logging.js +123 -0
- package/dist/runner/agentInvocation.d.ts +23 -0
- package/dist/runner/agentInvocation.js +106 -0
- package/dist/runner/changeCapture.d.ts +70 -0
- package/dist/runner/changeCapture.js +278 -6
- package/dist/runner/runTask.js +271 -65
- package/dist/runner/simpleProcess.d.ts +21 -0
- package/dist/runner/simpleProcess.js +206 -0
- package/dist/runner/watch.js +17 -2
- package/dist/security/commandGuard.d.ts +2 -1
- package/dist/security/commandGuard.js +25 -3
- package/dist/security/riskEngine.d.ts +27 -0
- package/dist/security/riskEngine.js +118 -0
- package/dist/security/runtimeGuard.d.ts +6 -0
- package/dist/security/runtimeGuard.js +28 -0
- package/dist/smoke-test.js +1353 -7
- package/dist/test/unit/android-doctor.test.d.ts +1 -0
- package/dist/test/unit/android-doctor.test.js +118 -0
- package/dist/test/unit/chinese-path.test.d.ts +1 -0
- package/dist/test/unit/chinese-path.test.js +91 -0
- package/dist/test/unit/command-guard.test.d.ts +1 -0
- package/dist/test/unit/command-guard.test.js +160 -0
- package/dist/test/unit/direct-guards.test.d.ts +1 -0
- package/dist/test/unit/direct-guards.test.js +213 -0
- package/dist/test/unit/logging.test.d.ts +1 -0
- package/dist/test/unit/logging.test.js +275 -0
- package/dist/test/unit/path-guard.test.d.ts +1 -0
- package/dist/test/unit/path-guard.test.js +109 -0
- package/dist/test/unit/safe-status.test.d.ts +1 -0
- package/dist/test/unit/safe-status.test.js +165 -0
- package/dist/test/unit/sensitive-guard.test.d.ts +1 -0
- package/dist/test/unit/sensitive-guard.test.js +104 -0
- package/dist/test/unit/sync-file.test.d.ts +1 -0
- package/dist/test/unit/sync-file.test.js +154 -0
- package/dist/test/unit/watcher-status.test.d.ts +1 -0
- package/dist/test/unit/watcher-status.test.js +169 -0
- package/dist/tools/androidDoctor.d.ts +38 -0
- package/dist/tools/androidDoctor.js +391 -0
- package/dist/tools/applyPatch.d.ts +16 -0
- package/dist/tools/applyPatch.js +41 -0
- package/dist/tools/auditSession.d.ts +5 -0
- package/dist/tools/auditSession.js +12 -0
- package/dist/tools/auditTask.d.ts +7 -0
- package/dist/tools/auditTask.js +105 -7
- package/dist/tools/createDirectSession.d.ts +14 -0
- package/dist/tools/createDirectSession.js +54 -0
- package/dist/tools/createTask.d.ts +48 -1
- package/dist/tools/createTask.js +298 -47
- package/dist/tools/finalizeDirectSession.d.ts +19 -0
- package/dist/tools/finalizeDirectSession.js +84 -0
- package/dist/tools/getTaskSummary.d.ts +50 -0
- package/dist/tools/getTaskSummary.js +79 -2
- package/dist/tools/healthCheck.d.ts +27 -12
- package/dist/tools/healthCheck.js +44 -6
- package/dist/tools/readWorkspaceFile.d.ts +7 -1
- package/dist/tools/readWorkspaceFile.js +48 -2
- package/dist/tools/registry.js +270 -19
- package/dist/tools/runVerification.d.ts +16 -0
- package/dist/tools/runVerification.js +32 -0
- package/dist/tools/safeStatus.d.ts +19 -0
- package/dist/tools/safeStatus.js +72 -0
- package/dist/tools/savePlan.d.ts +1 -0
- package/dist/tools/savePlan.js +38 -7
- package/dist/tools/searchWorkspace.d.ts +19 -0
- package/dist/tools/searchWorkspace.js +205 -0
- package/dist/tools/syncFile.d.ts +18 -0
- package/dist/tools/syncFile.js +65 -0
- package/dist/tools/taskOutputs.d.ts +2 -2
- package/dist/tools/taskTemplates.js +2 -1
- package/dist/tools/toolCatalog.d.ts +4 -3
- package/dist/tools/toolCatalog.js +34 -5
- package/dist/tools/waitForTask.d.ts +2 -2
- package/dist/tools/waitForTask.js +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/watcherStatus.d.ts +1 -0
- package/dist/watcherStatus.js +96 -4
- package/docs/chatgpt-usage.md +101 -0
- package/docs/performance-notes.md +55 -0
- package/docs/release-checklist.md +14 -0
- package/docs/release-v0.6.0.md +71 -0
- package/docs/release-v0.6.1.md +75 -0
- package/examples/config.example.json +5 -0
- package/examples/openai-tunnel/README.md +11 -4
- package/examples/openai-tunnel/chatgpt-test-prompt.md +18 -14
- package/examples/openai-tunnel/tunnel-client.example.yaml +8 -0
- package/package.json +13 -8
- package/scripts/brand-check.js +58 -12
- package/scripts/control-smoke.js +206 -0
- package/scripts/http-mcp-smoke.js +10 -2
- package/scripts/launchers/Check-PatchWarden-Health.cmd +6 -0
- package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +6 -0
- package/scripts/launchers/Restart-PatchWarden.cmd +6 -0
- package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +7 -0
- package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +7 -0
- package/scripts/lifecycle-smoke.js +402 -8
- package/scripts/manage-patchwarden.ps1 +639 -0
- package/scripts/mcp-manifest-check.js +123 -58
- package/scripts/mcp-smoke.js +160 -2
- package/scripts/pack-clean.js +160 -5
- package/scripts/package-manifest-check.js +78 -0
- package/scripts/patchwarden-mcp-direct.cmd +7 -0
- package/scripts/patchwarden-mcp-stdio.cmd +1 -1
- package/scripts/restart-patchwarden.ps1 +5 -4
- package/scripts/start-patchwarden-tunnel.ps1 +261 -30
- package/scripts/tunnel-supervisor-smoke.js +36 -4
- package/scripts/unit-tests.js +36 -0
- package/scripts/watcher-supervisor-smoke.js +10 -6
- package/src/assessments/agentAssessor.ts +324 -0
- package/src/assessments/assessmentStore.ts +426 -0
- package/src/assessments/confirmCli.ts +29 -0
- package/src/config.ts +178 -4
- package/src/direct/directAudit.ts +400 -0
- package/src/direct/directGuards.ts +279 -0
- package/src/direct/directPatch.ts +258 -0
- package/src/direct/directSessionStore.ts +345 -0
- package/src/direct/directVerification.ts +138 -0
- package/src/doctor.ts +103 -7
- package/src/logging.ts +152 -0
- package/src/runner/agentInvocation.ts +125 -0
- package/src/runner/changeCapture.ts +352 -6
- package/src/runner/runTask.ts +279 -63
- package/src/runner/simpleProcess.ts +223 -0
- package/src/runner/watch.ts +18 -2
- package/src/security/commandGuard.ts +46 -4
- package/src/security/riskEngine.ts +160 -0
- package/src/security/runtimeGuard.ts +41 -0
- package/src/smoke-test.ts +1291 -4
- package/src/test/unit/android-doctor.test.ts +158 -0
- package/src/test/unit/chinese-path.test.ts +106 -0
- package/src/test/unit/command-guard.test.ts +221 -0
- package/src/test/unit/direct-guards.test.ts +297 -0
- package/src/test/unit/logging.test.ts +325 -0
- package/src/test/unit/path-guard.test.ts +150 -0
- package/src/test/unit/safe-status.test.ts +187 -0
- package/src/test/unit/sensitive-guard.test.ts +124 -0
- package/src/test/unit/sync-file.test.ts +231 -0
- package/src/test/unit/watcher-status.test.ts +190 -0
- package/src/tools/androidDoctor.ts +424 -0
- package/src/tools/applyPatch.ts +86 -0
- package/src/tools/auditSession.ts +28 -0
- package/src/tools/auditTask.ts +111 -7
- package/src/tools/createDirectSession.ts +113 -0
- package/src/tools/createTask.ts +405 -55
- package/src/tools/finalizeDirectSession.ts +144 -0
- package/src/tools/getTaskSummary.ts +133 -2
- package/src/tools/healthCheck.ts +45 -6
- package/src/tools/readWorkspaceFile.ts +85 -2
- package/src/tools/registry.ts +305 -19
- package/src/tools/runVerification.ts +58 -0
- package/src/tools/safeStatus.ts +96 -0
- package/src/tools/savePlan.ts +57 -7
- package/src/tools/searchWorkspace.ts +275 -0
- package/src/tools/syncFile.ts +122 -0
- package/src/tools/taskTemplates.ts +2 -1
- package/src/tools/toolCatalog.ts +37 -6
- package/src/tools/waitForTask.ts +3 -3
- package/src/version.ts +2 -2
- package/src/watcherStatus.ts +101 -4
- package/tsconfig.json +18 -17
- package/Check-PatchWarden-Health.cmd +0 -6
- package/Reset-PatchWarden-Tunnel-Key.cmd +0 -6
- package/Restart-PatchWarden.cmd +0 -19
- package/Start-PatchWarden-Tunnel.cmd +0 -7
- package/docs/release-v0.3.0.md +0 -43
- package/docs/release-v0.4.0.md +0 -74
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
const isWindows = process.platform === "win32";
|
|
6
|
+
const command = isWindows ? (process.env.ComSpec || "cmd.exe") : "npm";
|
|
7
|
+
const args = isWindows
|
|
8
|
+
? ["/d", "/s", "/c", "npm.cmd pack --dry-run --json"]
|
|
9
|
+
: ["pack", "--dry-run", "--json"];
|
|
10
|
+
const result = spawnSync(command, args, {
|
|
11
|
+
encoding: "utf8",
|
|
12
|
+
shell: false,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (result.status !== 0) {
|
|
16
|
+
process.stderr.write(result.stderr || result.stdout || result.error?.message || "npm pack --dry-run --json failed\n");
|
|
17
|
+
process.exit(result.status || 1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let metadata;
|
|
21
|
+
try {
|
|
22
|
+
metadata = JSON.parse(result.stdout);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error(`[package-manifest-check] Could not parse npm pack JSON: ${error.message}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const files = metadata?.[0]?.files?.map((entry) => String(entry.path).replace(/\\/g, "/")) || [];
|
|
29
|
+
const forbidden = [
|
|
30
|
+
/(^|\/)\.local(\/|$)/i,
|
|
31
|
+
/\.local\.(cmd|ps1)$/i,
|
|
32
|
+
/(^|\/)patchwarden\.config\.json$/i,
|
|
33
|
+
new RegExp(`(^|/)${["safe", "bifrost"].join("-")}\\.config\\.json$`, "i"),
|
|
34
|
+
/(^|\/)\.env$/i,
|
|
35
|
+
/\.dpapi$/i,
|
|
36
|
+
/^docs\/optimization-proposal\.md$/i,
|
|
37
|
+
/(^|\/)kill-patchwarden\.(cmd|ps1)$/i,
|
|
38
|
+
];
|
|
39
|
+
const leaked = files.filter((file) => forbidden.some((pattern) => pattern.test(file)));
|
|
40
|
+
if (leaked.length > 0) {
|
|
41
|
+
console.error("[package-manifest-check] Private files would enter the npm package:");
|
|
42
|
+
for (const file of leaked) console.error(` ${file}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const required = [
|
|
47
|
+
"PatchWarden.cmd",
|
|
48
|
+
"scripts/manage-patchwarden.ps1",
|
|
49
|
+
"scripts/launchers/Start-PatchWarden-Tunnel.cmd",
|
|
50
|
+
"scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd",
|
|
51
|
+
];
|
|
52
|
+
const missing = required.filter((file) => !files.includes(file));
|
|
53
|
+
if (missing.length > 0) {
|
|
54
|
+
console.error("[package-manifest-check] Required control files are missing:");
|
|
55
|
+
for (const file of missing) console.error(` ${file}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const publicControlFiles = [
|
|
60
|
+
"PatchWarden.cmd",
|
|
61
|
+
"scripts/launchers/Start-PatchWarden-Tunnel.cmd",
|
|
62
|
+
"scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd",
|
|
63
|
+
];
|
|
64
|
+
const privateAbsolutePath = /[A-Za-z]:\\(?:Users\\[^\\\r\n]+|ai_agent)\\/i;
|
|
65
|
+
const privatePathLeaks = publicControlFiles.filter((file) => {
|
|
66
|
+
try {
|
|
67
|
+
return privateAbsolutePath.test(readFileSync(file, "utf8"));
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
if (privatePathLeaks.length > 0) {
|
|
73
|
+
console.error("[package-manifest-check] Public control files contain machine-specific absolute paths:");
|
|
74
|
+
for (const file of privatePathLeaks) console.error(` ${file}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(`[package-manifest-check] OK: ${files.length} package files, no private local launchers.`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
@echo off
|
|
2
2
|
setlocal
|
|
3
3
|
set "PROJECT_ROOT=%~dp0.."
|
|
4
|
-
set "PATCHWARDEN_CONFIG=%PROJECT_ROOT%\patchwarden.config.json"
|
|
4
|
+
if not defined PATCHWARDEN_CONFIG set "PATCHWARDEN_CONFIG=%PROJECT_ROOT%\patchwarden.config.json"
|
|
5
5
|
set "PATCHWARDEN_TOOL_PROFILE=chatgpt_core"
|
|
6
6
|
cd /d "%PROJECT_ROOT%"
|
|
7
7
|
"node" "%PROJECT_ROOT%\dist\index.js"
|
|
@@ -196,7 +196,7 @@ Write-Host ""
|
|
|
196
196
|
Write-Host "[4/4] Launching PatchWarden tunnel..." -ForegroundColor Yellow
|
|
197
197
|
|
|
198
198
|
if ($WhatIf) {
|
|
199
|
-
Write-Host " WHAT-IF: Would launch
|
|
199
|
+
Write-Host " WHAT-IF: Would launch the Core compatibility launcher." -ForegroundColor Magenta
|
|
200
200
|
Write-Host ""
|
|
201
201
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
202
202
|
Write-Host " Restart plan complete (--WhatIf)." -ForegroundColor Cyan
|
|
@@ -207,8 +207,9 @@ if ($WhatIf) {
|
|
|
207
207
|
|
|
208
208
|
# Relaunch the tunnel in a new PowerShell window.
|
|
209
209
|
# Prefer Start-PatchWarden-Tunnel.local.cmd (your saved config) over the generic launcher.
|
|
210
|
-
$
|
|
211
|
-
$
|
|
210
|
+
$launcherDirectory = Join-Path $ProjectRoot "scripts\launchers"
|
|
211
|
+
$localLauncher = Join-Path $launcherDirectory "Start-PatchWarden-Tunnel.local.cmd"
|
|
212
|
+
$genericLauncher = Join-Path $launcherDirectory "Start-PatchWarden-Tunnel.cmd"
|
|
212
213
|
$launcherPath = if (Test-Path -LiteralPath $localLauncher) { $localLauncher } else { $genericLauncher }
|
|
213
214
|
|
|
214
215
|
if (-not (Test-Path -LiteralPath $launcherPath)) {
|
|
@@ -237,6 +238,6 @@ Start-Process powershell.exe -ArgumentList $args
|
|
|
237
238
|
|
|
238
239
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
239
240
|
Write-Host " Restart complete. Tunnel launcher window opened." -ForegroundColor Cyan
|
|
240
|
-
Write-Host " Use
|
|
241
|
+
Write-Host " Use PatchWarden.cmd status all to verify." -ForegroundColor Cyan
|
|
241
242
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
242
243
|
Write-Host ""
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
param(
|
|
2
2
|
[string]$TunnelId = $env:PATCHWARDEN_TUNNEL_ID,
|
|
3
3
|
[string]$Profile = "patchwarden",
|
|
4
|
+
[ValidateSet("chatgpt_core", "chatgpt_direct")]
|
|
5
|
+
[string]$ToolProfile = "chatgpt_core",
|
|
4
6
|
[string]$ProxyUrl = $(if ($env:HTTPS_PROXY) { $env:HTTPS_PROXY } else { "http://127.0.0.1:7892" }),
|
|
5
7
|
[string]$TunnelClientExe = $env:TUNNEL_CLIENT_EXE,
|
|
6
8
|
[string]$OpencodeBin = $env:OPENCODE_BIN_DIR,
|
|
9
|
+
[string]$ConfigPath = $env:PATCHWARDEN_CONFIG,
|
|
7
10
|
[string]$CredentialPath = $(if ($env:PATCHWARDEN_CREDENTIAL_PATH) { $env:PATCHWARDEN_CREDENTIAL_PATH } else { Join-Path $env:APPDATA "patchwarden\control-plane-api-key.dpapi" }),
|
|
8
11
|
[int]$ReconnectBaseSeconds = 5,
|
|
9
12
|
[int]$ReconnectMaxSeconds = 30,
|
|
@@ -12,21 +15,38 @@ param(
|
|
|
12
15
|
[int]$WatcherMaxRestartAttempts = 5,
|
|
13
16
|
[int]$WatcherHealthyResetSeconds = 60,
|
|
14
17
|
[switch]$SkipWatcher,
|
|
15
|
-
[switch]$ForgetSavedApiKey
|
|
18
|
+
[switch]$ForgetSavedApiKey,
|
|
19
|
+
[string]$HealthListenAddr = ""
|
|
16
20
|
)
|
|
17
21
|
|
|
18
22
|
$ErrorActionPreference = "Stop"
|
|
19
23
|
|
|
24
|
+
if ([string]::IsNullOrWhiteSpace($HealthListenAddr)) {
|
|
25
|
+
if ($Profile -eq "patchwarden-direct" -or $ToolProfile -eq "chatgpt_direct") {
|
|
26
|
+
$HealthListenAddr = "127.0.0.1:8081"
|
|
27
|
+
} else {
|
|
28
|
+
$HealthListenAddr = "127.0.0.1:8080"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
$ProjectRoot = Split-Path -Parent $PSScriptRoot
|
|
21
|
-
|
|
22
|
-
$
|
|
33
|
+
if (-not $ConfigPath) {
|
|
34
|
+
$ConfigPath = Join-Path $ProjectRoot "patchwarden.config.json"
|
|
35
|
+
}
|
|
36
|
+
$McpLauncherName = if ($ToolProfile -eq "chatgpt_direct") { "patchwarden-mcp-direct.cmd" } else { "patchwarden-mcp-stdio.cmd" }
|
|
37
|
+
$McpStdioLauncher = Join-Path $ProjectRoot "scripts\$McpLauncherName"
|
|
23
38
|
$McpStdioLauncherForTunnel = $McpStdioLauncher -replace "\\", "/"
|
|
24
39
|
$OpencodeConfigHome = Join-Path $env:LOCALAPPDATA "patchwarden\opencode-config"
|
|
25
40
|
$ProfilePath = Join-Path $env:APPDATA "tunnel-client\$Profile.yaml"
|
|
26
|
-
$
|
|
41
|
+
$RuntimeName = if ($ToolProfile -eq "chatgpt_direct") { "runtime-direct" } else { "runtime" }
|
|
42
|
+
$RuntimeDirectory = Join-Path $env:LOCALAPPDATA "patchwarden\$RuntimeName"
|
|
27
43
|
$StatusFile = Join-Path $RuntimeDirectory "tunnel-status.json"
|
|
28
44
|
$HealthUrlFile = Join-Path $RuntimeDirectory "tunnel-health-url.txt"
|
|
29
45
|
$PidFile = Join-Path $RuntimeDirectory "tunnel-client.pid"
|
|
46
|
+
$StdoutLogFile = Join-Path $RuntimeDirectory "tunnel-client.stdout.log"
|
|
47
|
+
$StderrLogFile = Join-Path $RuntimeDirectory "tunnel-client.stderr.log"
|
|
48
|
+
$LegacyPidFile = Join-Path $env:TEMP $(if ($ToolProfile -eq "chatgpt_direct") { "patchwarden-direct.pid" } else { "patchwarden-core.pid" })
|
|
49
|
+
$LegacyHealthUrlFile = Join-Path $env:TEMP $(if ($ToolProfile -eq "chatgpt_direct") { "patchwarden-direct-health.url" } else { "patchwarden-core-health.url" })
|
|
30
50
|
$WatcherStatusFile = Join-Path $RuntimeDirectory "watcher-status.json"
|
|
31
51
|
$script:PendingCredential = $null
|
|
32
52
|
$script:TunnelProcess = $null
|
|
@@ -38,6 +58,10 @@ $script:WatcherRestartAttempts = 0
|
|
|
38
58
|
$script:WatcherHealthySince = $null
|
|
39
59
|
$script:WatcherRestartExhausted = $false
|
|
40
60
|
|
|
61
|
+
if ($ToolProfile -eq "chatgpt_direct") {
|
|
62
|
+
$SkipWatcher = $true
|
|
63
|
+
}
|
|
64
|
+
|
|
41
65
|
function Assert-File {
|
|
42
66
|
param([string]$Path, [string]$Name)
|
|
43
67
|
if (-not (Test-Path -LiteralPath $Path)) {
|
|
@@ -104,6 +128,52 @@ function Save-PendingCredential {
|
|
|
104
128
|
Write-Host "[saved] Encrypted credential cache: $CredentialPath"
|
|
105
129
|
}
|
|
106
130
|
|
|
131
|
+
function Protect-DiagnosticText {
|
|
132
|
+
param([AllowNull()][string]$Text)
|
|
133
|
+
if ($null -eq $Text) { return $null }
|
|
134
|
+
$safe = [string]$Text
|
|
135
|
+
if ($env:CONTROL_PLANE_API_KEY) {
|
|
136
|
+
$safe = $safe.Replace([string]$env:CONTROL_PLANE_API_KEY, "[REDACTED]")
|
|
137
|
+
}
|
|
138
|
+
$safe = $safe -replace '(?i)(authorization\s*:\s*bearer\s+)\S+', '$1[REDACTED]'
|
|
139
|
+
$safe = $safe -replace '(?i)(CONTROL_PLANE_API_KEY\s*[=:]\s*)\S+', '$1[REDACTED]'
|
|
140
|
+
return $safe
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function Get-LogTail {
|
|
144
|
+
param([string]$Path, [int]$LineCount = 30)
|
|
145
|
+
if (-not (Test-Path -LiteralPath $Path)) { return @() }
|
|
146
|
+
try {
|
|
147
|
+
return @(
|
|
148
|
+
Get-Content -LiteralPath $Path -Tail $LineCount -Encoding UTF8 -ErrorAction Stop |
|
|
149
|
+
ForEach-Object { Protect-DiagnosticText -Text ([string]$_) }
|
|
150
|
+
)
|
|
151
|
+
} catch {
|
|
152
|
+
return @("Could not read log tail: $($_.Exception.Message)")
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function Protect-LogFile {
|
|
157
|
+
param([string]$Path)
|
|
158
|
+
if (-not (Test-Path -LiteralPath $Path)) { return }
|
|
159
|
+
try {
|
|
160
|
+
$content = Get-Content -LiteralPath $Path -Raw -Encoding UTF8 -ErrorAction Stop
|
|
161
|
+
$protected = Protect-DiagnosticText -Text $content
|
|
162
|
+
if ($protected -ne $content) {
|
|
163
|
+
Set-Content -LiteralPath $Path -Value $protected -Encoding UTF8 -NoNewline
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
Write-Host "[warn] Could not sanitize diagnostic log $Path`: $($_.Exception.Message)" -ForegroundColor Yellow
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function Get-LastDiagnosticLine {
|
|
171
|
+
param([string[]]$Lines, [string]$Fallback)
|
|
172
|
+
$meaningful = @($Lines | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
|
173
|
+
if ($meaningful.Count -gt 0) { return [string]$meaningful[-1] }
|
|
174
|
+
return $Fallback
|
|
175
|
+
}
|
|
176
|
+
|
|
107
177
|
function Write-TunnelStatus {
|
|
108
178
|
param(
|
|
109
179
|
[string]$Status,
|
|
@@ -112,10 +182,14 @@ function Write-TunnelStatus {
|
|
|
112
182
|
[int]$Attempt = 0,
|
|
113
183
|
[Nullable[int]]$ProcessId = $null,
|
|
114
184
|
[string]$LastError = $null,
|
|
115
|
-
[string]$NextRetryAt = $null
|
|
185
|
+
[string]$NextRetryAt = $null,
|
|
186
|
+
[Nullable[int]]$ProcessExitCode = $null,
|
|
187
|
+
[string[]]$StdoutTail = @(),
|
|
188
|
+
[string[]]$StderrTail = @()
|
|
116
189
|
)
|
|
117
190
|
New-Item -ItemType Directory -Force -Path $RuntimeDirectory | Out-Null
|
|
118
|
-
$
|
|
191
|
+
$protectedError = Protect-DiagnosticText -Text $LastError
|
|
192
|
+
$safeError = if ($protectedError) { ($protectedError -replace '[\r\n]+', ' ').Substring(0, [Math]::Min(500, ($protectedError -replace '[\r\n]+', ' ').Length)) } else { $null }
|
|
119
193
|
$payload = [ordered]@{
|
|
120
194
|
status = $Status
|
|
121
195
|
reason_code = $ReasonCode
|
|
@@ -125,13 +199,19 @@ function Write-TunnelStatus {
|
|
|
125
199
|
checked_at = (Get-Date).ToUniversalTime().ToString("o")
|
|
126
200
|
next_retry_at = $NextRetryAt
|
|
127
201
|
last_error = $safeError
|
|
202
|
+
last_exit_code = $ProcessExitCode
|
|
203
|
+
stdout_tail = @($StdoutTail)
|
|
204
|
+
stderr_tail = @($StderrTail)
|
|
205
|
+
stdout_log = $StdoutLogFile
|
|
206
|
+
stderr_log = $StderrLogFile
|
|
128
207
|
server_version = if ($script:ToolManifest) { $script:ToolManifest.server_version } else { $null }
|
|
129
208
|
schema_epoch = if ($script:ToolManifest) { $script:ToolManifest.schema_epoch } else { $null }
|
|
130
209
|
tool_profile = if ($script:ToolManifest) { $script:ToolManifest.tool_profile } else { $null }
|
|
131
210
|
tool_count = if ($script:ToolManifest) { $script:ToolManifest.tool_count } else { $null }
|
|
132
211
|
tool_names = if ($script:ToolManifest) { $script:ToolManifest.tool_names } else { @() }
|
|
133
212
|
tool_manifest_sha256 = if ($script:ToolManifest) { $script:ToolManifest.tool_manifest_sha256 } else { $null }
|
|
134
|
-
|
|
213
|
+
tools_ready = [bool]($script:ToolManifest -and $script:ToolManifest.ok)
|
|
214
|
+
core_tools_ready = [bool]($ToolProfile -eq "chatgpt_core" -and $script:ToolManifest -and $script:ToolManifest.ok)
|
|
135
215
|
}
|
|
136
216
|
$temporary = "$StatusFile.tmp"
|
|
137
217
|
$payload | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $temporary -Encoding UTF8
|
|
@@ -215,19 +295,30 @@ function Stop-OwnedWatcherProcess {
|
|
|
215
295
|
function Start-OwnedWatcherProcess {
|
|
216
296
|
$script:WatcherInstanceId = [Guid]::NewGuid().ToString("n")
|
|
217
297
|
$env:PATCHWARDEN_CONFIG = $ConfigPath
|
|
218
|
-
$env:PATCHWARDEN_WATCHER_INSTANCE_ID = $script:WatcherInstanceId
|
|
219
|
-
$env:PATCHWARDEN_WATCHER_LAUNCHER_PID = [string]$PID
|
|
220
|
-
$env:XDG_CONFIG_HOME = $OpencodeConfigHome
|
|
221
|
-
if ($OpencodeBin) { $env:PATH = "$OpencodeBin;$env:PATH" }
|
|
222
298
|
$node = (Get-Command node.exe -ErrorAction Stop).Source
|
|
223
299
|
$stdout = Join-Path $RuntimeDirectory "watcher-$($script:WatcherInstanceId).stdout.log"
|
|
224
300
|
$stderr = Join-Path $RuntimeDirectory "watcher-$($script:WatcherInstanceId).stderr.log"
|
|
225
301
|
$script:WatcherManaged = $true
|
|
226
302
|
$script:WatcherHealthySince = $null
|
|
227
|
-
$
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
303
|
+
$previousWatcherInstanceId = $env:PATCHWARDEN_WATCHER_INSTANCE_ID
|
|
304
|
+
$previousWatcherLauncherPid = $env:PATCHWARDEN_WATCHER_LAUNCHER_PID
|
|
305
|
+
$previousXdgConfigHome = $env:XDG_CONFIG_HOME
|
|
306
|
+
$previousPath = $env:PATH
|
|
307
|
+
try {
|
|
308
|
+
$env:PATCHWARDEN_WATCHER_INSTANCE_ID = $script:WatcherInstanceId
|
|
309
|
+
$env:PATCHWARDEN_WATCHER_LAUNCHER_PID = [string]$PID
|
|
310
|
+
$env:XDG_CONFIG_HOME = $OpencodeConfigHome
|
|
311
|
+
if ($OpencodeBin) { $env:PATH = "$OpencodeBin;$env:PATH" }
|
|
312
|
+
$script:WatcherProcess = Start-Process -FilePath $node `
|
|
313
|
+
-ArgumentList @((Join-Path $ProjectRoot "dist\runner\watch.js")) `
|
|
314
|
+
-WorkingDirectory $ProjectRoot -PassThru -WindowStyle Hidden `
|
|
315
|
+
-RedirectStandardOutput $stdout -RedirectStandardError $stderr
|
|
316
|
+
} finally {
|
|
317
|
+
if ($null -eq $previousWatcherInstanceId) { Remove-Item Env:PATCHWARDEN_WATCHER_INSTANCE_ID -ErrorAction SilentlyContinue } else { $env:PATCHWARDEN_WATCHER_INSTANCE_ID = $previousWatcherInstanceId }
|
|
318
|
+
if ($null -eq $previousWatcherLauncherPid) { Remove-Item Env:PATCHWARDEN_WATCHER_LAUNCHER_PID -ErrorAction SilentlyContinue } else { $env:PATCHWARDEN_WATCHER_LAUNCHER_PID = $previousWatcherLauncherPid }
|
|
319
|
+
if ($null -eq $previousXdgConfigHome) { Remove-Item Env:XDG_CONFIG_HOME -ErrorAction SilentlyContinue } else { $env:XDG_CONFIG_HOME = $previousXdgConfigHome }
|
|
320
|
+
$env:PATH = $previousPath
|
|
321
|
+
}
|
|
231
322
|
Write-WatcherStatus -Status "starting"
|
|
232
323
|
Write-Host "[watch] Started owned watcher PID $($script:WatcherProcess.Id), instance $($script:WatcherInstanceId)."
|
|
233
324
|
}
|
|
@@ -280,17 +371,119 @@ function Get-TunnelHealth {
|
|
|
280
371
|
}
|
|
281
372
|
|
|
282
373
|
function Stop-OwnedTunnelProcess {
|
|
374
|
+
if (Test-Path -LiteralPath $PidFile) {
|
|
375
|
+
$rawPid = $null
|
|
376
|
+
try { $rawPid = (Get-Content -LiteralPath $PidFile -Raw -ErrorAction Stop).Trim() } catch {}
|
|
377
|
+
$childPid = 0
|
|
378
|
+
if ($rawPid -and [int]::TryParse($rawPid, [ref]$childPid) -and $childPid -gt 0) {
|
|
379
|
+
$childProcess = Get-ProcessByIdSafe -ProcessId $childPid
|
|
380
|
+
if ($childProcess -and (Test-TunnelProcessForProfile -Process $childProcess)) {
|
|
381
|
+
Stop-Process -Id $childPid -ErrorAction SilentlyContinue
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
283
385
|
if ($script:TunnelProcess -and -not $script:TunnelProcess.HasExited) {
|
|
284
386
|
Stop-Process -Id $script:TunnelProcess.Id -ErrorAction SilentlyContinue
|
|
285
387
|
try { $script:TunnelProcess.WaitForExit(5000) | Out-Null } catch {}
|
|
286
388
|
}
|
|
287
389
|
}
|
|
288
390
|
|
|
391
|
+
function Get-ProcessByIdSafe {
|
|
392
|
+
param([Nullable[int]]$ProcessId)
|
|
393
|
+
if (-not $ProcessId -or $ProcessId -le 0) { return $null }
|
|
394
|
+
try {
|
|
395
|
+
return Get-CimInstance Win32_Process -Filter "ProcessId = $ProcessId" -ErrorAction Stop
|
|
396
|
+
} catch {
|
|
397
|
+
return $null
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function Test-TunnelProcessForProfile {
|
|
402
|
+
param($Process)
|
|
403
|
+
if (-not $Process -or [string]$Process.Name -ine "tunnel-client.exe") { return $false }
|
|
404
|
+
$commandLine = [string]$Process.CommandLine
|
|
405
|
+
if (-not $commandLine) { return $false }
|
|
406
|
+
$profilePattern = '(?i)"?--profile"?(?:=|\s+)"?' + [Regex]::Escape($Profile) + '(?:"|\s|$)'
|
|
407
|
+
$normalizedCommand = $commandLine -replace '\\', '/'
|
|
408
|
+
$normalizedProfilePath = $ProfilePath -replace '\\', '/'
|
|
409
|
+
return $commandLine -match $profilePattern -or $normalizedCommand.IndexOf($normalizedProfilePath, [StringComparison]::OrdinalIgnoreCase) -ge 0
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function Clear-StaleRunFiles {
|
|
413
|
+
foreach ($candidatePidFile in @($PidFile, $LegacyPidFile)) {
|
|
414
|
+
if (-not (Test-Path -LiteralPath $candidatePidFile)) { continue }
|
|
415
|
+
$rawPid = $null
|
|
416
|
+
try { $rawPid = (Get-Content -LiteralPath $candidatePidFile -Raw -ErrorAction Stop).Trim() } catch {}
|
|
417
|
+
$parsedPid = 0
|
|
418
|
+
if ($rawPid -and [int]::TryParse($rawPid, [ref]$parsedPid) -and $parsedPid -gt 0) {
|
|
419
|
+
$existingProcess = Get-ProcessByIdSafe -ProcessId $parsedPid
|
|
420
|
+
if ($existingProcess) {
|
|
421
|
+
if (Test-TunnelProcessForProfile -Process $existingProcess) {
|
|
422
|
+
$modeName = if ($ToolProfile -eq "chatgpt_direct") { "direct" } else { "core" }
|
|
423
|
+
throw "[conflict] Existing tunnel-client PID $parsedPid still uses profile $Profile. Run PatchWarden.cmd restart $modeName or kill it explicitly."
|
|
424
|
+
}
|
|
425
|
+
Write-Host "[cleanup] Stale PID file $candidatePidFile references unrelated PID $parsedPid ($($existingProcess.Name)); the process will not be stopped." -ForegroundColor Yellow
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
foreach ($path in @($PidFile, $HealthUrlFile, $LegacyPidFile, $LegacyHealthUrlFile)) {
|
|
431
|
+
if (Test-Path -LiteralPath $path) {
|
|
432
|
+
Remove-Item -LiteralPath $path -Force -ErrorAction Stop
|
|
433
|
+
Write-Host "[cleanup] Removed stale runtime file: $path"
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function Get-TrackedTunnelProcessId {
|
|
439
|
+
if (Test-Path -LiteralPath $PidFile) {
|
|
440
|
+
$rawPid = $null
|
|
441
|
+
try { $rawPid = (Get-Content -LiteralPath $PidFile -Raw -ErrorAction Stop).Trim() } catch {}
|
|
442
|
+
$trackedPid = 0
|
|
443
|
+
if ($rawPid -and [int]::TryParse($rawPid, [ref]$trackedPid) -and $trackedPid -gt 0) {
|
|
444
|
+
$trackedProcess = Get-ProcessByIdSafe -ProcessId $trackedPid
|
|
445
|
+
if ($trackedProcess -and (Test-TunnelProcessForProfile -Process $trackedProcess)) {
|
|
446
|
+
return $trackedPid
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if ($script:TunnelProcess -and -not $script:TunnelProcess.HasExited) { return $script:TunnelProcess.Id }
|
|
451
|
+
return $null
|
|
452
|
+
}
|
|
453
|
+
|
|
289
454
|
function Quote-ProcessArgument {
|
|
290
455
|
param([string]$Value)
|
|
291
456
|
return '"' + ($Value -replace '"', '\"') + '"'
|
|
292
457
|
}
|
|
293
458
|
|
|
459
|
+
function Set-ProfileHealthListenAddr {
|
|
460
|
+
param([string]$ProfilePath, [string]$HealthListenAddr)
|
|
461
|
+
if (-not (Test-Path -LiteralPath $ProfilePath)) { return }
|
|
462
|
+
$content = Get-Content -LiteralPath $ProfilePath -Raw
|
|
463
|
+
$target = [Regex]::Escape($HealthListenAddr)
|
|
464
|
+
# Already correct; nothing to do (idempotent)
|
|
465
|
+
if ($content -match "listen_addr:\s*`"$target`"") { return }
|
|
466
|
+
# Has a health block with a different listen_addr; replace the value
|
|
467
|
+
if ($content -match 'listen_addr:\s*"[^"]*"') {
|
|
468
|
+
$updated = $content -replace '(listen_addr:\s*)"[^"]*"', "`$1`"$HealthListenAddr`""
|
|
469
|
+
Set-Content -LiteralPath $ProfilePath -Value $updated -Encoding UTF8 -NoNewline
|
|
470
|
+
Write-Host "[config] Updated health.listen_addr to $HealthListenAddr in $Profile"
|
|
471
|
+
return
|
|
472
|
+
}
|
|
473
|
+
# Has a health: section but no listen_addr; insert after health:
|
|
474
|
+
if ($content -match '(?m)^health:') {
|
|
475
|
+
$updated = $content -replace '(?m)^(health:.*)$', "`$1`n listen_addr: `"$HealthListenAddr`""
|
|
476
|
+
Set-Content -LiteralPath $ProfilePath -Value $updated -Encoding UTF8 -NoNewline
|
|
477
|
+
Write-Host "[config] Added health.listen_addr: $HealthListenAddr to existing health block in $Profile"
|
|
478
|
+
return
|
|
479
|
+
}
|
|
480
|
+
# No health block at all; append one
|
|
481
|
+
$trimmed = $content.TrimEnd()
|
|
482
|
+
$updated = "$trimmed`n`nhealth:`n listen_addr: `"$HealthListenAddr`"`n"
|
|
483
|
+
Set-Content -LiteralPath $ProfilePath -Value $updated -Encoding UTF8 -NoNewline
|
|
484
|
+
Write-Host "[config] Added health block with listen_addr: $HealthListenAddr to $Profile"
|
|
485
|
+
}
|
|
486
|
+
|
|
294
487
|
if ($ForgetSavedApiKey) {
|
|
295
488
|
if (Test-Path -LiteralPath $CredentialPath) {
|
|
296
489
|
Remove-Item -LiteralPath $CredentialPath -Force
|
|
@@ -312,21 +505,21 @@ if (-not $TunnelClientExe) {
|
|
|
312
505
|
$TunnelClientExe = Read-Host "Path to tunnel-client.exe"
|
|
313
506
|
}
|
|
314
507
|
|
|
315
|
-
if (-not $OpencodeBin) {
|
|
508
|
+
if (-not $SkipWatcher -and -not $OpencodeBin) {
|
|
316
509
|
$candidateOpencodeBin = Join-Path $env:APPDATA "npm\node_modules\opencode-ai\bin"
|
|
317
510
|
if (Test-Path -LiteralPath $candidateOpencodeBin) {
|
|
318
511
|
$OpencodeBin = $candidateOpencodeBin
|
|
319
512
|
}
|
|
320
513
|
}
|
|
321
514
|
|
|
322
|
-
if (-not $OpencodeBin) {
|
|
515
|
+
if (-not $SkipWatcher -and -not $OpencodeBin) {
|
|
323
516
|
Write-Host "[warn] OPENCODE_BIN_DIR is not set and opencode-ai bin was not found under APPDATA."
|
|
324
517
|
Write-Host " Watcher will still start, but opencode tasks may fail unless opencode is on PATH."
|
|
325
518
|
}
|
|
326
519
|
|
|
327
520
|
Assert-File -Path $TunnelClientExe -Name "tunnel-client.exe"
|
|
328
521
|
Assert-File -Path $ConfigPath -Name "patchwarden.config.json"
|
|
329
|
-
Assert-File -Path $McpStdioLauncher -Name
|
|
522
|
+
Assert-File -Path $McpStdioLauncher -Name $McpLauncherName
|
|
330
523
|
|
|
331
524
|
if (-not $TunnelId) {
|
|
332
525
|
$TunnelId = Read-Host "Tunnel ID"
|
|
@@ -344,8 +537,8 @@ if (-not (Test-Path -LiteralPath (Join-Path $ProjectRoot "dist\index.js"))) {
|
|
|
344
537
|
|
|
345
538
|
New-Item -ItemType Directory -Force -Path $RuntimeDirectory | Out-Null
|
|
346
539
|
$env:PATCHWARDEN_CONFIG = $ConfigPath
|
|
347
|
-
Write-Host "[manifest] Verifying the exact
|
|
348
|
-
$manifestOutput = (& node (Join-Path $ProjectRoot "scripts\mcp-manifest-check.js") --json 2>&1 | Out-String).Trim()
|
|
540
|
+
Write-Host "[manifest] Verifying the exact $ToolProfile stdio MCP tool catalog..."
|
|
541
|
+
$manifestOutput = (& node (Join-Path $ProjectRoot "scripts\mcp-manifest-check.js") --profile $ToolProfile --json 2>&1 | Out-String).Trim()
|
|
349
542
|
if ($LASTEXITCODE -ne 0) {
|
|
350
543
|
Write-TunnelStatus -Status "stopped" -ReasonCode "tool_manifest_check_failed" -LastError "The tunnel MCP tool manifest preflight failed."
|
|
351
544
|
throw "Tool manifest preflight failed: $manifestOutput"
|
|
@@ -359,6 +552,9 @@ try {
|
|
|
359
552
|
$manifestFile = Join-Path $RuntimeDirectory "tool-manifest.json"
|
|
360
553
|
$manifestOutput | Set-Content -LiteralPath $manifestFile -Encoding UTF8
|
|
361
554
|
Write-Host "[manifest] $($script:ToolManifest.server_version) profile=$($script:ToolManifest.tool_profile) tools=$($script:ToolManifest.tool_count) hash=$($script:ToolManifest.tool_manifest_sha256)"
|
|
555
|
+
Write-Host "[config] tunnel profile: $Profile"
|
|
556
|
+
Write-Host "[config] tool profile: $ToolProfile"
|
|
557
|
+
Write-Host "[config] health listen addr: $HealthListenAddr"
|
|
362
558
|
|
|
363
559
|
Set-SecretEnvIfMissing
|
|
364
560
|
|
|
@@ -382,6 +578,9 @@ if ($profileNeedsInit) {
|
|
|
382
578
|
--tunnel-id $TunnelId `
|
|
383
579
|
--mcp-command $McpStdioLauncherForTunnel `
|
|
384
580
|
--force
|
|
581
|
+
Set-ProfileHealthListenAddr -ProfilePath $ProfilePath -HealthListenAddr $HealthListenAddr
|
|
582
|
+
} else {
|
|
583
|
+
Set-ProfileHealthListenAddr -ProfilePath $ProfilePath -HealthListenAddr $HealthListenAddr
|
|
385
584
|
}
|
|
386
585
|
|
|
387
586
|
$env:PATCHWARDEN_TUNNEL_STATUS_FILE = $StatusFile
|
|
@@ -400,7 +599,7 @@ while ($doctor.ExitCode -ne 0 -and $doctor.ReasonCode -eq "transient_connection_
|
|
|
400
599
|
}
|
|
401
600
|
if ($doctor.ExitCode -ne 0) {
|
|
402
601
|
if ($doctor.ReasonCode -eq "transient_connection_failure") {
|
|
403
|
-
Write-Host "[doctor]
|
|
602
|
+
Write-Host "[doctor] Preflight transient; continuing to supervised run after $preflightAttempt retries." -ForegroundColor Yellow
|
|
404
603
|
Write-TunnelStatus -Status "degraded" -ReasonCode $doctor.ReasonCode -Ready $false -LastError "Preflight doctor reported transient connection failure; proceeding with supervisor-managed recovery."
|
|
405
604
|
} else {
|
|
406
605
|
Write-TunnelStatus -Status "stopped" -ReasonCode $doctor.ReasonCode -LastError "tunnel-client doctor failed; review the launcher output."
|
|
@@ -416,7 +615,18 @@ Save-PendingCredential
|
|
|
416
615
|
|
|
417
616
|
Write-Host ""
|
|
418
617
|
Write-Host "[run] Starting supervised tunnel-client. Keep this window open."
|
|
419
|
-
Write-Host "[health] Run
|
|
618
|
+
Write-Host "[health] Run PatchWarden.cmd status all if ChatGPT cannot reach the MCP."
|
|
619
|
+
Write-Host "[run-config] tunnel-client exe: $TunnelClientExe"
|
|
620
|
+
Write-Host "[run-config] profile: $Profile"
|
|
621
|
+
Write-Host "[run-config] health listen addr: $HealthListenAddr"
|
|
622
|
+
Write-Host "[run-config] health url file: $HealthUrlFile"
|
|
623
|
+
Write-Host "[run-config] pid file: $PidFile"
|
|
624
|
+
Write-Host "[run-config] stdout log: $StdoutLogFile"
|
|
625
|
+
Write-Host "[run-config] stderr log: $StderrLogFile"
|
|
626
|
+
Write-Host "[run-config] HTTPS_PROXY: $env:HTTPS_PROXY"
|
|
627
|
+
Write-Host "[run-config] HTTP_PROXY: $env:HTTP_PROXY"
|
|
628
|
+
Write-Host "[run-config] CONTROL_PLANE_API_KEY: $(if ($env:CONTROL_PLANE_API_KEY) { 'set' } else { 'missing' })"
|
|
629
|
+
Write-Host "[run-config] cwd: $ProjectRoot"
|
|
420
630
|
Write-Host ""
|
|
421
631
|
|
|
422
632
|
$attempt = 0
|
|
@@ -424,7 +634,8 @@ $openUi = $true
|
|
|
424
634
|
try {
|
|
425
635
|
while ($true) {
|
|
426
636
|
$attempt++
|
|
427
|
-
|
|
637
|
+
Clear-StaleRunFiles
|
|
638
|
+
Remove-Item -LiteralPath $StdoutLogFile, $StderrLogFile -Force -ErrorAction SilentlyContinue
|
|
428
639
|
Write-TunnelStatus -Status "starting" -ReasonCode $null -Ready $false -Attempt $attempt
|
|
429
640
|
$runArguments = @(
|
|
430
641
|
"run", "--profile", $Profile,
|
|
@@ -435,9 +646,13 @@ try {
|
|
|
435
646
|
)
|
|
436
647
|
if ($openUi) { $runArguments += "--open-web-ui"; $openUi = $false }
|
|
437
648
|
$argumentLine = ($runArguments | ForEach-Object { Quote-ProcessArgument $_ }) -join " "
|
|
438
|
-
|
|
649
|
+
Write-Host "[run-config] command: tunnel-client $($runArguments -join ' ')"
|
|
650
|
+
$redirectedCommand = '""' + $TunnelClientExe + '" ' + $argumentLine + ' 1>"' + $StdoutLogFile + '" 2>"' + $StderrLogFile + '""'
|
|
651
|
+
$processArgumentLine = '/d /s /c ' + $redirectedCommand
|
|
652
|
+
$script:TunnelProcess = Start-Process -FilePath $env:ComSpec -ArgumentList $processArgumentLine `
|
|
653
|
+
-WorkingDirectory $ProjectRoot -PassThru -WindowStyle Hidden
|
|
439
654
|
Write-TunnelStatus -Status "connecting" -ReasonCode $null -Ready $false -Attempt $attempt -ProcessId $script:TunnelProcess.Id
|
|
440
|
-
Write-Host "[run]
|
|
655
|
+
Write-Host "[run] supervised command PID $($script:TunnelProcess.Id), attempt $attempt"
|
|
441
656
|
|
|
442
657
|
$unreadySince = $null
|
|
443
658
|
$restartForHealth = $false
|
|
@@ -447,11 +662,11 @@ try {
|
|
|
447
662
|
$health = Get-TunnelHealth
|
|
448
663
|
if ($health -and $health.healthz.ok -and $health.readyz.ok) {
|
|
449
664
|
$unreadySince = $null
|
|
450
|
-
Write-TunnelStatus -Status "ready" -ReasonCode $null -Ready $true -Attempt $attempt -ProcessId
|
|
665
|
+
Write-TunnelStatus -Status "ready" -ReasonCode $null -Ready $true -Attempt $attempt -ProcessId (Get-TrackedTunnelProcessId)
|
|
451
666
|
continue
|
|
452
667
|
}
|
|
453
668
|
if (-not $unreadySince) { $unreadySince = Get-Date }
|
|
454
|
-
Write-TunnelStatus -Status "degraded" -ReasonCode "tunnel_not_ready" -Ready $false -Attempt $attempt -ProcessId
|
|
669
|
+
Write-TunnelStatus -Status "degraded" -ReasonCode "tunnel_not_ready" -Ready $false -Attempt $attempt -ProcessId (Get-TrackedTunnelProcessId) -LastError "Tunnel process is running but /readyz is not ready."
|
|
455
670
|
if (((Get-Date) - $unreadySince).TotalSeconds -ge $UnreadyRestartSeconds) {
|
|
456
671
|
$doctor = Invoke-TunnelDoctor
|
|
457
672
|
if ($doctor.ExitCode -ne 0 -and $doctor.ReasonCode -ne "transient_connection_failure") {
|
|
@@ -466,21 +681,37 @@ try {
|
|
|
466
681
|
}
|
|
467
682
|
}
|
|
468
683
|
|
|
684
|
+
try { [void]$script:TunnelProcess.WaitForExit(5000) } catch {}
|
|
685
|
+
try { $script:TunnelProcess.Refresh() } catch {}
|
|
469
686
|
$exitCode = $script:TunnelProcess.ExitCode
|
|
687
|
+
Protect-LogFile -Path $StdoutLogFile
|
|
688
|
+
Protect-LogFile -Path $StderrLogFile
|
|
689
|
+
$stdoutTail = @(Get-LogTail -Path $StdoutLogFile -LineCount 30)
|
|
690
|
+
$stderrTail = @(Get-LogTail -Path $StderrLogFile -LineCount 30)
|
|
691
|
+
$exitError = Get-LastDiagnosticLine -Lines $stderrTail -Fallback "Tunnel process exited with code $exitCode."
|
|
692
|
+
if ($exitCode -ne 0) {
|
|
693
|
+
Write-Host "[error] tunnel-client exited with code $exitCode." -ForegroundColor Red
|
|
694
|
+
Write-Host "[error] stderr tail:" -ForegroundColor Red
|
|
695
|
+
if ($stderrTail.Count -gt 0) {
|
|
696
|
+
$stderrTail | ForEach-Object { Write-Host $_ -ForegroundColor DarkRed }
|
|
697
|
+
} else {
|
|
698
|
+
Write-Host "(stderr log was empty)" -ForegroundColor DarkRed
|
|
699
|
+
}
|
|
700
|
+
}
|
|
470
701
|
$doctor = Invoke-TunnelDoctor
|
|
471
702
|
if ($doctor.ExitCode -ne 0 -and $doctor.ReasonCode -ne "transient_connection_failure") {
|
|
472
|
-
Write-TunnelStatus -Status "stopped" -ReasonCode $doctor.ReasonCode -Ready $false -Attempt $attempt -LastError
|
|
703
|
+
Write-TunnelStatus -Status "stopped" -ReasonCode $doctor.ReasonCode -Ready $false -Attempt $attempt -LastError $exitError -ProcessExitCode $exitCode -StdoutTail $stdoutTail -StderrTail $stderrTail
|
|
473
704
|
Write-Host "[doctor] Non-retryable diagnostic: $($doctor.ReasonCode)"
|
|
474
705
|
throw "Tunnel stopped after non-retryable error: $($doctor.ReasonCode)"
|
|
475
706
|
}
|
|
476
707
|
if ($MaxReconnectAttempts -gt 0 -and $attempt -ge $MaxReconnectAttempts) {
|
|
477
|
-
Write-TunnelStatus -Status "stopped" -ReasonCode "retry_limit_reached" -Ready $false -Attempt $attempt -LastError
|
|
708
|
+
Write-TunnelStatus -Status "stopped" -ReasonCode "retry_limit_reached" -Ready $false -Attempt $attempt -LastError $exitError -ProcessExitCode $exitCode -StdoutTail $stdoutTail -StderrTail $stderrTail
|
|
478
709
|
throw "Tunnel reconnect attempt limit reached."
|
|
479
710
|
}
|
|
480
711
|
$delay = [Math]::Min($ReconnectMaxSeconds, $ReconnectBaseSeconds * [Math]::Pow(2, [Math]::Min(3, $attempt - 1)))
|
|
481
712
|
$nextRetry = (Get-Date).AddSeconds($delay).ToUniversalTime().ToString("o")
|
|
482
713
|
$reason = if ($restartForHealth) { "tunnel_not_ready" } else { "tunnel_process_exited" }
|
|
483
|
-
Write-TunnelStatus -Status "reconnecting" -ReasonCode $reason -Ready $false -Attempt $attempt -LastError
|
|
714
|
+
Write-TunnelStatus -Status "reconnecting" -ReasonCode $reason -Ready $false -Attempt $attempt -LastError $exitError -NextRetryAt $nextRetry -ProcessExitCode $exitCode -StdoutTail $stdoutTail -StderrTail $stderrTail
|
|
484
715
|
Write-Host "[retry] tunnel-client exited with code $exitCode; retrying in $delay seconds."
|
|
485
716
|
Start-Sleep -Seconds $delay
|
|
486
717
|
}
|