Files
isphere-ai-bridge/scripts/package-login-reachability-probe.ps1
2026-07-12 14:08:25 +08:00

1024 lines
40 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
param(
[string]$OutputDir = "runs/login-reachability-probe-package",
[switch]$NoZip,
[switch]$CopyToDownloads
)
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
$repo = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path
$packageDir = Join-Path $repo $OutputDir
$zipPath = "$packageDir.zip"
$recorderRelative = Join-Path $OutputDir "recorder"
$recorderDir = Join-Path $packageDir "recorder"
$toolsDir = Join-Path $packageDir "tools"
$docsDir = Join-Path $packageDir "docs"
$evidenceDir = Join-Path $packageDir "return-evidence"
$capabilityExe = Join-Path $toolsDir "isphere-capability-smoke.exe"
if (Test-Path -LiteralPath $packageDir) {
Remove-Item -LiteralPath $packageDir -Recurse -Force
}
if (Test-Path -LiteralPath $zipPath) {
Remove-Item -LiteralPath $zipPath -Force
}
New-Item -ItemType Directory -Force -Path $packageDir | Out-Null
New-Item -ItemType Directory -Force -Path $toolsDir | Out-Null
New-Item -ItemType Directory -Force -Path $docsDir | Out-Null
New-Item -ItemType Directory -Force -Path $evidenceDir | Out-Null
& powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $repo "scripts\package-live-probe-recorder.ps1") -OutputDir $recorderRelative -NoZip
if ($LASTEXITCODE -ne 0) {
throw "package-live-probe-recorder.ps1 failed with exit code $LASTEXITCODE"
}
if (-not (Test-Path -LiteralPath (Join-Path $recorderDir "ISphereLiveProbeRecorder.exe"))) {
throw "recorder package was not created under $recorderDir"
}
Push-Location $repo
try {
& go build -o $capabilityExe ./cmd/isphere-capability-smoke
if ($LASTEXITCODE -ne 0) {
throw "go build ./cmd/isphere-capability-smoke failed with exit code $LASTEXITCODE"
}
}
finally {
Pop-Location
}
if (-not (Test-Path -LiteralPath $capabilityExe)) {
throw "capability smoke executable was not created: $capabilityExe"
}
Copy-Item -LiteralPath (Join-Path $repo "docs\source-discovery\2026-07-12-login-reachability-validation-items.md") -Destination (Join-Path $docsDir "login-reachability-validation-items.md") -Force
Copy-Item -LiteralPath (Join-Path $repo "docs\source-discovery\2026-07-12-send-upload-deep-dive.md") -Destination (Join-Path $docsDir "send-upload-deep-dive.md") -Force
@'
iSphere Login Reachability Probe Package
LOCAL_LOGIN_UNAVAILABLE: 本开发机无法登录 iSphere。
ONLINE_LOGIN_REQUIRED: 只在可以正常登录 iSphere/IMPlatformClient 的电脑和网络里运行。
NO_AUTOMATIC_SEND: 本包不会自动发消息、不会自动上传文件。
业务目标:
- 明天在可登录环境里一次性采到 B-route 需要的证据;
- 覆盖文字发送、文件发送、sent-record/ack、运行态/服务端参数、失败原因;
- 继续保持生产 send/file send 禁用,直到返回证据通过机器校验。
推荐一键流程:
1. 解压本 zip 到普通目录。
2. 手工打开并登录 iSphere/IMPlatformClient。
3. 准备一个 direct 测试目标,可选准备一个 group 测试目标。
4. 准备一段一次性测试文字和一个安全小文件。
5. 双击 00-prepare-inputs.bat按说明填写 LOGIN-REACHABILITY-INPUTS.json。
6. 双击 RUN-LOGIN-REACHABILITY-SUITE.bat按暂停提示操作。
7. 把生成的 isphere-login-reachability-return-*.zip 发回。
分步流程:
- 01-run-before-recorder.bat发送/上传前采集。
- 02-run-after-text-recorder.bat手工发送一次文字后采集。
- 03-run-after-file-recorder.bat手工发送一次文件后采集。
- 04-create-return-zip.bat生成返回 zip。
它会记录:
- 目标进程、位数、模块、窗口、UIA 控件树;
- 发送入口静态可达性;
- TCP 连接、安装/缓存/配置/日志/数据库候选文件元数据;
- operator 填写的 one-shot 目标、hash、sent-record/ack、结构化失败原因
- LOGIN-REACHABILITY-OUTPUT.json 顶层 schemarun/targets/runtime/reachability/server_auth/snapshots/attempts/failures/correlation。
它不会:
- 自动发送消息;
- 自动发送或上传文件;
- 点击或输入;
- 注入或 hook
- 修改客户端文件、数据库或配置;
- 收集明文消息正文或 token。
'@ | Set-Content -LiteralPath (Join-Path $packageDir "README.txt") -Encoding UTF8
@'
# 2026-07-13 登录可达性探针 - 小白使用说明
## 先记住三句话
1. 必须先手工登录 iSphere/IMPlatformClient。
2. 本工具不会帮你发送,只负责前后采集;发送动作由你在官方客户端里手工做。
3. 文字只发一次,文件只发一次;不要重复发。
## 第一步:准备输入
双击:
```text
00-prepare-inputs.bat
```
它会复制模板并打开 `LOGIN-REACHABILITY-INPUTS.json`。
你至少要填:
- `run.operator_confirmed_logged_in`: true
- `run.one_shot_text_enabled`: true 或 false
- `run.one_shot_file_enabled`: true 或 false
- `targets.direct_jid`: direct 测试对象
- `targets.group_jid`: 可选群组测试对象
- `targets.target_type`: direct 或 group
如果要测文字:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\compute-text-hash.ps1 -Text "你准备发的那句测试文字"
```
把输出填到:
- `text_attempt.content_sha256`
- `text_attempt.content_length`
不要把明文测试文字写进 JSON。
如果要测文件:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\compute-file-hash.ps1 -Path "C:\path\to\test-file.txt"
```
把输出里的 hash/size 填进:
- `file_attempt.sandbox_file_path`
- `file_attempt.file_sha256`
- `file_attempt.file_size_bytes`
## 第二步:运行 before 采集
双击:
```text
01-run-before-recorder.bat
```
看到完成后,确认 `return-evidence\before` 里有文件。
## 第三步:手工发送一次文字
如果 `run.one_shot_text_enabled=true`
1. 在官方 iSphere 客户端里打开目标会话。
2. 手工发送刚才计算 hash 的那一句测试文字。
3. 不要发第二次。
4. 填写 `LOGIN-REACHABILITY-INPUTS.json` 里的:
- `run.one_text_send_only_confirmed`: true
- `text_attempt.operator_clicked_send_at_local`
- `text_attempt.success_ack_or_sent_record_present`
- `text_attempt.success_ack_or_sent_record`
- `text_attempt.duplicate_second_send_attempted`: false
如果失败,也要填 `text_attempt.failure` 里的 step/code/classification/message/next_action。
然后双击:
```text
02-run-after-text-recorder.bat
```
## 第四步:手工发送一次文件
如果 `run.one_shot_file_enabled=true`
1. 在官方 iSphere 客户端里打开目标会话。
2. 手工发送刚才计算 hash 的那一个安全小文件。
3. 不要发第二个文件,也不要重复发同一个文件。
4. 填写 `LOGIN-REACHABILITY-INPUTS.json` 里的:
- `run.one_file_send_only_confirmed`: true
- `file_attempt.operator_clicked_send_file_at_local`
- `file_attempt.file_upload_or_sent_record_present`
- `file_attempt.file_upload_or_sent_record`
- `file_attempt.duplicate_second_file_send_attempted`: false
如果失败,也要填 `file_attempt.failure` 里的 step/code/classification/message/next_action。
然后双击:
```text
03-run-after-file-recorder.bat
```
## 第五步:生成返回包
双击:
```text
04-create-return-zip.bat
```
它会生成:
```text
isphere-login-reachability-return-时间戳.zip
```
把这个 zip 发回。
## 如果你不知道怎么填失败分类
常用 classification
- offline
- permission
- target_not_found
- token_missing
- file_size
- file_type
- network_timeout
- upload_failed
- ack_missing
- sent_record_missing
- unknown
'@ | Set-Content -LiteralPath (Join-Path $packageDir "OPERATOR-STEPS.md") -Encoding UTF8
@'
{
"schema_version": "2026-07-13.login-reachability.v1",
"run": {
"operator_confirmed_logged_in": false,
"operator_started_at_local": "",
"operator_finished_at_local": "",
"before_recorder_present": false,
"one_shot_text_enabled": true,
"one_shot_file_enabled": true,
"one_text_send_only_confirmed": false,
"one_file_send_only_confirmed": false,
"production_send_enabled": false,
"production_file_send_enabled": false
},
"targets": {
"target_type": "direct",
"direct_jid": "",
"group_jid": "",
"target_selection_source": "",
"self_send_or_sandbox_account_confirmed": false
},
"adapter_reachability": {
"mode_observed": "unknown",
"same_process_adapter_possible": "unknown",
"managed_harness_required": "unknown",
"injection_required": "unknown",
"stop_condition_reason": ""
},
"server_auth": {
"host": "",
"port": "",
"domain": "",
"token_present": null,
"token_hash_prefix": "",
"access_token_present": null,
"client_id_present": null
},
"sent_record_source": {
"source_type": "",
"source_path_or_ref": "",
"before_count": "",
"after_count": "",
"ack_or_ref_id": "",
"correlation_notes": ""
},
"runtime_observations": {
"app_context_manager_reachable": null,
"message_scheduling_reachable": null,
"chat_xmpp_authenticated": null,
"target_jid_mapping_observed": null,
"file_upload_para_observed": null,
"ifile_transfer_reachable": null,
"file_upload_result_file_id_observed": null,
"send_file_message_finalization_observed": null,
"notes": "",
"failure": {
"step": "",
"code": "",
"classification": "",
"message": "",
"next_action": ""
}
},
"text_attempt": {
"enabled": true,
"content_sha256": "",
"content_length": 0,
"operator_clicked_send_at_local": "",
"after_recorder_present": false,
"success_ack_or_sent_record_present": false,
"success_ack_or_sent_record": "",
"duplicate_second_send_attempted": false,
"failure": {
"step": "",
"code": "",
"classification": "",
"message": "",
"next_action": ""
}
},
"file_attempt": {
"enabled": true,
"allowed_dir": "",
"sandbox_file_path": "",
"file_sha256": "",
"file_size_bytes": 0,
"operator_clicked_send_file_at_local": "",
"after_recorder_present": false,
"file_upload_or_sent_record_present": false,
"file_upload_or_sent_record": "",
"duplicate_second_file_send_attempted": false,
"failure": {
"step": "",
"code": "",
"classification": "",
"message": "",
"next_action": ""
}
},
"group_probe": {
"enabled": false,
"group_jid": "",
"chatroom_branch_observed": null,
"speech_permission_state": "",
"upload_permission_state": "",
"mute_or_permission_failure_reason": "",
"failure": {
"step": "",
"code": "",
"classification": "",
"message": "",
"next_action": ""
}
},
"fallback_a_route": {
"enabled_if_b_route_fails": true,
"hwnd": "",
"rtbSendMessage_present": null,
"btnSend_present": null,
"btnSendFile_or_file_menu_present": null,
"uia_or_screenshot_ref": "",
"failure": {
"step": "",
"code": "",
"classification": "",
"message": "",
"next_action": ""
}
},
"operator_notes": ""
}
'@ | Set-Content -LiteralPath (Join-Path $packageDir "LOGIN-REACHABILITY-INPUTS.template.json") -Encoding UTF8
@'
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "iSphere login reachability probe input",
"type": "object",
"required": ["schema_version", "run", "targets", "runtime_observations", "text_attempt", "file_attempt", "group_probe", "fallback_a_route"],
"properties": {
"schema_version": { "const": "2026-07-13.login-reachability.v1" },
"run": {
"type": "object",
"required": ["operator_confirmed_logged_in", "before_recorder_present", "one_shot_text_enabled", "one_shot_file_enabled", "production_send_enabled", "production_file_send_enabled"],
"properties": {
"operator_confirmed_logged_in": { "type": "boolean" },
"before_recorder_present": { "type": "boolean" },
"one_shot_text_enabled": { "type": "boolean" },
"one_shot_file_enabled": { "type": "boolean" },
"production_send_enabled": { "const": false },
"production_file_send_enabled": { "const": false }
}
},
"targets": {
"type": "object",
"required": ["target_type", "direct_jid"],
"properties": {
"target_type": { "type": "string" },
"direct_jid": { "type": "string" },
"group_jid": { "type": "string" }
}
},
"runtime_observations": { "type": "object" },
"text_attempt": { "type": "object" },
"file_attempt": { "type": "object" },
"group_probe": { "type": "object" },
"fallback_a_route": { "type": "object" }
}
}
'@ | Set-Content -LiteralPath (Join-Path $packageDir "LOGIN-REACHABILITY-INPUTS.schema.json") -Encoding UTF8
@'
Expected return files:
Required:
- LOGIN-REACHABILITY-INPUTS.json
- LOGIN-REACHABILITY-OUTPUT.json
- return-evidence/before/*
Required when text_attempt.enabled=true:
- return-evidence/after-text/*
- text_attempt.content_sha256
- text_attempt.operator_clicked_send_at_local
- text_attempt.duplicate_second_send_attempted=false
- either text_attempt.success_ack_or_sent_record_present=true plus success text, or structured text_attempt.failure
Required when file_attempt.enabled=true:
- return-evidence/after-file/*
- file_attempt.sandbox_file_path
- file_attempt.file_sha256
- file_attempt.file_size_bytes
- file_attempt.duplicate_second_file_send_attempted=false
- either file_attempt.file_upload_or_sent_record_present=true plus result text, or structured file_attempt.failure
Required for remaining validation TODO coverage:
- runtime_observations records AppContextManager/MessageScheduling/Chat-XMPP/FileUploadPara/IFileTransfer/FileID/sendFileMessage observations or a structured failure.
- group_probe records ChatRoom branch and permissions when group_jid is used.
- fallback_a_route records hwnd/rtbSendMessage/btnSend/file-menu evidence when B-route fails and A-route evidence is needed.
Decision:
- Production text send remains blocked unless one-shot target + before/after snapshot + sent-record source + content-hash/ack correlation pass.
- Production file send remains blocked unless FileUploadPara + IFileTransfer/FileUpload + FileUploadResult.FileID + file-message finalization + file sent-record/ack correlation pass.
- A-route/RPA remains backup-only.
'@ | Set-Content -LiteralPath (Join-Path $packageDir "EXPECTED-RETURN-FILES.txt") -Encoding UTF8
@'
# TODO-COVERAGE
This package covers the remaining validation TODO items from the 2026-07-12 login reachability schema.
P0 coverage:
- one-shot test target: LOGIN-REACHABILITY-INPUTS.json / targets
- before/after sent-record snapshots: return-evidence/before, after-text, after-file plus sent_record_source
- sent-record and ack source: sent_record_source
- in-process adapter reachability mode: adapter_reachability
- logged-in runtime and loaded assemblies: recorder/probe_client_runtime.json
- AppContextManager/IAppContextManager: runtime_observations plus send_entrypoints_preflight.json
- Chat/XMPPConnection authenticated state: runtime_observations plus send_entrypoints_preflight.json
- FileUploadPara values: server_auth plus runtime_observations
- IFileTransfer/FileUpload reachability: runtime_observations plus send_entrypoints_preflight.json
- FileUploadResult.FileID: runtime_observations and file_attempt evidence/failure
- trans_UploadCompleted/SendFileMessage finalization: runtime_observations and file_attempt evidence/failure
- structured failure reasons: text_attempt.failure, file_attempt.failure, runtime_observations.failure, group_probe.failure, fallback_a_route.failure
- sent record/ack correlation: correlation output section
P1 coverage:
- group ChatRoom branch and permissions: group_probe
- A-route fallback UI evidence: fallback_a_route
The package still does not auto-send, auto-upload, click, type, inject, hook, or enable production send.
'@ | Set-Content -LiteralPath (Join-Path $packageDir "TODO-COVERAGE.md") -Encoding UTF8
@'
@echo off
setlocal
cd /d "%~dp0"
if not exist LOGIN-REACHABILITY-INPUTS.json copy LOGIN-REACHABILITY-INPUTS.template.json LOGIN-REACHABILITY-INPUTS.json >nul
echo 已创建/确认 LOGIN-REACHABILITY-INPUTS.json
echo 请按 OPERATOR-STEPS.md 填写,然后保存。
notepad LOGIN-REACHABILITY-INPUTS.json
pause
'@ | Set-Content -LiteralPath (Join-Path $packageDir "00-prepare-inputs.bat") -Encoding ASCII
@'
@echo off
setlocal
cd /d "%~dp0"
mkdir return-evidence 2>nul
mkdir return-evidence\before 2>nul
echo Running BEFORE read-only recorder...
recorder\ISphereLiveProbeRecorder.exe --record-live-probe --out "%~dp0return-evidence\before" --no-pause
echo before recorder exit code: %ERRORLEVEL% > return-evidence\before\exit-code.txt
echo Done. Check return-evidence\before.
pause
exit /b 0
'@ | Set-Content -LiteralPath (Join-Path $packageDir "01-run-before-recorder.bat") -Encoding ASCII
@'
@echo off
setlocal
cd /d "%~dp0"
mkdir return-evidence 2>nul
mkdir return-evidence\after-text 2>nul
echo Run this AFTER the one manual text send.
recorder\ISphereLiveProbeRecorder.exe --record-live-probe --out "%~dp0return-evidence\after-text" --no-pause
echo after text recorder exit code: %ERRORLEVEL% > return-evidence\after-text\exit-code.txt
echo Done. Check return-evidence\after-text.
pause
exit /b 0
'@ | Set-Content -LiteralPath (Join-Path $packageDir "02-run-after-text-recorder.bat") -Encoding ASCII
@'
@echo off
setlocal
cd /d "%~dp0"
mkdir return-evidence 2>nul
mkdir return-evidence\after-file 2>nul
echo Run this AFTER the one manual file send.
recorder\ISphereLiveProbeRecorder.exe --record-live-probe --out "%~dp0return-evidence\after-file" --no-pause
echo after file recorder exit code: %ERRORLEVEL% > return-evidence\after-file\exit-code.txt
echo Done. Check return-evidence\after-file.
pause
exit /b 0
'@ | Set-Content -LiteralPath (Join-Path $packageDir "03-run-after-file-recorder.bat") -Encoding ASCII
@'
@echo off
setlocal
cd /d "%~dp0"
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0CREATE-RETURN-ZIP.ps1"
echo exit code: %ERRORLEVEL%
pause
exit /b %ERRORLEVEL%
'@ | Set-Content -LiteralPath (Join-Path $packageDir "04-create-return-zip.bat") -Encoding ASCII
@'
@echo off
setlocal
cd /d "%~dp0"
echo iSphere login reachability recording suite
echo.
call "%~dp000-prepare-inputs.bat"
echo.
echo Step 1: before recorder
call "%~dp001-run-before-recorder.bat"
echo.
echo Step 2: if text_attempt.enabled=true, manually send exactly one text message in official iSphere.
echo Fill text_attempt fields in LOGIN-REACHABILITY-INPUTS.json before continuing.
pause
call "%~dp002-run-after-text-recorder.bat"
echo.
echo Step 3: if file_attempt.enabled=true, manually send exactly one safe small file in official iSphere.
echo Fill file_attempt fields in LOGIN-REACHABILITY-INPUTS.json before continuing.
pause
call "%~dp003-run-after-file-recorder.bat"
echo.
echo Step 4: create return zip
call "%~dp004-create-return-zip.bat"
exit /b %ERRORLEVEL%
'@ | Set-Content -LiteralPath (Join-Path $packageDir "RUN-LOGIN-REACHABILITY-SUITE.bat") -Encoding ASCII
@'
param(
[Parameter(Mandatory=$true)]
[string]$Text
)
$bytes = [System.Text.Encoding]::UTF8.GetBytes($Text)
$sha = [System.Security.Cryptography.SHA256]::Create()
try {
$hash = $sha.ComputeHash($bytes)
[ordered]@{
content_sha256 = (-join ($hash | ForEach-Object { $_.ToString("x2") }))
content_length = $Text.Length
note = "只把 hash 和长度填入 JSON不要填写明文内容。"
} | ConvertTo-Json -Depth 4
}
finally {
$sha.Dispose()
}
'@ | Set-Content -LiteralPath (Join-Path $packageDir "compute-text-hash.ps1") -Encoding ASCII
@'
param(
[Parameter(Mandatory=$true)]
[string]$Path
)
$resolved = Resolve-Path -LiteralPath $Path
$item = Get-Item -LiteralPath $resolved.Path
[ordered]@{
sandbox_file_path = $resolved.Path
file_sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $resolved.Path).Hash.ToLowerInvariant()
file_size_bytes = $item.Length
note = "确认这是安全小文件,并且只发送一次。"
} | ConvertTo-Json -Depth 4
'@ | Set-Content -LiteralPath (Join-Path $packageDir "compute-file-hash.ps1") -Encoding ASCII
@'
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
$inputsPath = Join-Path $root "LOGIN-REACHABILITY-INPUTS.json"
$outputPath = Join-Path $root "LOGIN-REACHABILITY-OUTPUT.json"
$evidenceRoot = Join-Path $root "return-evidence"
function Test-NonEmptyString($Value) {
if ($null -eq $Value) { return $false }
return -not [string]::IsNullOrWhiteSpace([string]$Value)
}
function Test-Hash64($Value) {
return (Test-NonEmptyString $Value) -and ([string]$Value -match '^[0-9a-fA-F]{64}$')
}
function Test-DirectoryHasFiles($Path) {
return (Test-Path -LiteralPath $Path) -and (@(Get-ChildItem -LiteralPath $Path -Recurse -File -ErrorAction SilentlyContinue).Count -gt 0)
}
function Find-LatestProbeFile($Stage, $Name) {
$stageDir = Join-Path $evidenceRoot $Stage
if (-not (Test-Path -LiteralPath $stageDir)) { return $null }
$file = Get-ChildItem -LiteralPath $stageDir -Recurse -File -Filter $Name -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
if ($null -eq $file) { return $null }
return $file.FullName
}
function Read-ProbeJson($Stage, $Name) {
$path = Find-LatestProbeFile -Stage $Stage -Name $Name
if ($null -eq $path) { return $null }
try {
return Get-Content -LiteralPath $path -Raw | ConvertFrom-Json
}
catch {
return $null
}
}
function Get-EntrypointMap($Preflight) {
$map = [ordered]@{}
if ($null -eq $Preflight -or $null -eq $Preflight.entrypoints) { return $map }
foreach ($entry in @($Preflight.entrypoints)) {
$map[[string]$entry.id] = [ordered]@{
capability = $entry.capability
assembly_found = $entry.assembly_found
reflection_type_found = $entry.reflection_type_found
reflection_method_found = $entry.reflection_method_found
string_type_hint = $entry.string_type_hint
string_method_hint = $entry.string_method_hint
available = $entry.available
reflection_error = $entry.reflection_error
}
}
return $map
}
function Add-ValidationFailure([System.Collections.Generic.List[object]]$List, [string]$Code, [string]$Message) {
$List.Add([ordered]@{
step = "return_zip_validation"
code = $Code
classification = "missing_or_invalid_input"
message = $Message
next_action = "补齐 LOGIN-REACHABILITY-INPUTS.json 或对应 return-evidence 目录后重新运行 04-create-return-zip.bat"
}) | Out-Null
}
function Test-StructuredFailure($Failure) {
if ($null -eq $Failure) { return $false }
return (Test-NonEmptyString $Failure.step) -and
(Test-NonEmptyString $Failure.code) -and
(Test-NonEmptyString $Failure.classification) -and
(Test-NonEmptyString $Failure.next_action)
}
function Test-ObservedOrStructuredFailure($Observed, $Failure) {
if ($Observed -eq $true) { return $true }
return Test-StructuredFailure $Failure
}
if (-not (Test-Path -LiteralPath $inputsPath)) {
throw "missing LOGIN-REACHABILITY-INPUTS.json; run 00-prepare-inputs.bat first"
}
if (-not (Test-Path -LiteralPath $evidenceRoot)) {
throw "missing return-evidence directory"
}
$inputs = Get-Content -LiteralPath $inputsPath -Raw | ConvertFrom-Json
$validationFailures = New-Object System.Collections.Generic.List[object]
if ($inputs.schema_version -ne "2026-07-13.login-reachability.v1") {
Add-ValidationFailure $validationFailures "BAD_SCHEMA_VERSION" "schema_version must be 2026-07-13.login-reachability.v1"
}
if ($inputs.run.operator_confirmed_logged_in -ne $true) {
Add-ValidationFailure $validationFailures "LOGIN_NOT_CONFIRMED" "run.operator_confirmed_logged_in must be true"
}
if ($inputs.run.production_send_enabled -ne $false) {
Add-ValidationFailure $validationFailures "PRODUCTION_SEND_ENABLED" "production_send_enabled must remain false"
}
if ($inputs.run.production_file_send_enabled -ne $false) {
Add-ValidationFailure $validationFailures "PRODUCTION_FILE_SEND_ENABLED" "production_file_send_enabled must remain false"
}
if ($inputs.run.before_recorder_present -ne $true) {
Add-ValidationFailure $validationFailures "BEFORE_RECORDER_FLAG_MISSING" "run.before_recorder_present must be true"
}
if (-not (Test-DirectoryHasFiles (Join-Path $evidenceRoot "before"))) {
Add-ValidationFailure $validationFailures "BEFORE_EVIDENCE_MISSING" "return-evidence/before must contain recorder output"
}
if (-not (Test-NonEmptyString $inputs.targets.direct_jid)) {
Add-ValidationFailure $validationFailures "DIRECT_TARGET_MISSING" "targets.direct_jid is required"
}
foreach ($field in @(
"app_context_manager_reachable",
"message_scheduling_reachable",
"chat_xmpp_authenticated",
"target_jid_mapping_observed",
"file_upload_para_observed",
"ifile_transfer_reachable",
"file_upload_result_file_id_observed",
"send_file_message_finalization_observed"
)) {
if (-not (Test-ObservedOrStructuredFailure $inputs.runtime_observations.$field $inputs.runtime_observations.failure)) {
Add-ValidationFailure $validationFailures ("RUNTIME_" + $field.ToUpperInvariant() + "_MISSING") ("runtime_observations." + $field + " must be true or runtime_observations.failure must be structured")
}
}
$textEnabled = ($inputs.run.one_shot_text_enabled -eq $true) -or ($inputs.text_attempt.enabled -eq $true)
if ($textEnabled) {
if ($inputs.run.one_text_send_only_confirmed -ne $true) {
Add-ValidationFailure $validationFailures "TEXT_ONE_SHOT_NOT_CONFIRMED" "run.one_text_send_only_confirmed must be true"
}
if (-not (Test-Hash64 $inputs.text_attempt.content_sha256)) {
Add-ValidationFailure $validationFailures "TEXT_HASH_MISSING" "text_attempt.content_sha256 must be a 64-char SHA256"
}
if ($inputs.text_attempt.content_length -le 0) {
Add-ValidationFailure $validationFailures "TEXT_LENGTH_MISSING" "text_attempt.content_length must be greater than 0"
}
if (-not (Test-NonEmptyString $inputs.text_attempt.operator_clicked_send_at_local)) {
Add-ValidationFailure $validationFailures "TEXT_CLICK_TIME_MISSING" "text_attempt.operator_clicked_send_at_local is required"
}
if ($inputs.text_attempt.after_recorder_present -ne $true) {
Add-ValidationFailure $validationFailures "TEXT_AFTER_FLAG_MISSING" "text_attempt.after_recorder_present must be true"
}
if (-not (Test-DirectoryHasFiles (Join-Path $evidenceRoot "after-text"))) {
Add-ValidationFailure $validationFailures "TEXT_AFTER_EVIDENCE_MISSING" "return-evidence/after-text must contain recorder output"
}
if ($inputs.text_attempt.duplicate_second_send_attempted -ne $false) {
Add-ValidationFailure $validationFailures "TEXT_DUPLICATE_ATTEMPTED" "text_attempt.duplicate_second_send_attempted must be false"
}
$textSuccess = ($inputs.text_attempt.success_ack_or_sent_record_present -eq $true) -and (Test-NonEmptyString $inputs.text_attempt.success_ack_or_sent_record)
$textFailure = (Test-NonEmptyString $inputs.text_attempt.failure.step) -and (Test-NonEmptyString $inputs.text_attempt.failure.code) -and (Test-NonEmptyString $inputs.text_attempt.failure.classification) -and (Test-NonEmptyString $inputs.text_attempt.failure.next_action)
if (-not $textSuccess -and -not $textFailure) {
Add-ValidationFailure $validationFailures "TEXT_RESULT_OR_FAILURE_MISSING" "text attempt needs either success ack/sent-record or structured failure"
}
}
$fileEnabled = ($inputs.run.one_shot_file_enabled -eq $true) -or ($inputs.file_attempt.enabled -eq $true)
if ($fileEnabled) {
if ($inputs.run.one_file_send_only_confirmed -ne $true) {
Add-ValidationFailure $validationFailures "FILE_ONE_SHOT_NOT_CONFIRMED" "run.one_file_send_only_confirmed must be true"
}
foreach ($field in @("allowed_dir", "sandbox_file_path", "operator_clicked_send_file_at_local")) {
if (-not (Test-NonEmptyString $inputs.file_attempt.$field)) {
Add-ValidationFailure $validationFailures ("FILE_" + $field.ToUpperInvariant() + "_MISSING") "file_attempt.$field is required"
}
}
if (-not (Test-Hash64 $inputs.file_attempt.file_sha256)) {
Add-ValidationFailure $validationFailures "FILE_HASH_MISSING" "file_attempt.file_sha256 must be a 64-char SHA256"
}
if ($inputs.file_attempt.file_size_bytes -le 0) {
Add-ValidationFailure $validationFailures "FILE_SIZE_MISSING" "file_attempt.file_size_bytes must be greater than 0"
}
if ($inputs.file_attempt.after_recorder_present -ne $true) {
Add-ValidationFailure $validationFailures "FILE_AFTER_FLAG_MISSING" "file_attempt.after_recorder_present must be true"
}
if (-not (Test-DirectoryHasFiles (Join-Path $evidenceRoot "after-file"))) {
Add-ValidationFailure $validationFailures "FILE_AFTER_EVIDENCE_MISSING" "return-evidence/after-file must contain recorder output"
}
if ($inputs.file_attempt.duplicate_second_file_send_attempted -ne $false) {
Add-ValidationFailure $validationFailures "FILE_DUPLICATE_ATTEMPTED" "file_attempt.duplicate_second_file_send_attempted must be false"
}
$fileSuccess = ($inputs.file_attempt.file_upload_or_sent_record_present -eq $true) -and (Test-NonEmptyString $inputs.file_attempt.file_upload_or_sent_record)
$fileFailure = (Test-NonEmptyString $inputs.file_attempt.failure.step) -and (Test-NonEmptyString $inputs.file_attempt.failure.code) -and (Test-NonEmptyString $inputs.file_attempt.failure.classification) -and (Test-NonEmptyString $inputs.file_attempt.failure.next_action)
if (-not $fileSuccess -and -not $fileFailure) {
Add-ValidationFailure $validationFailures "FILE_RESULT_OR_FAILURE_MISSING" "file attempt needs either upload/sent-record evidence or structured failure"
}
}
$groupEnabled = ($inputs.group_probe.enabled -eq $true) -or (Test-NonEmptyString $inputs.targets.group_jid)
if ($groupEnabled) {
if (-not (Test-NonEmptyString $inputs.group_probe.group_jid) -and -not (Test-NonEmptyString $inputs.targets.group_jid)) {
Add-ValidationFailure $validationFailures "GROUP_JID_MISSING" "group_probe.group_jid or targets.group_jid is required when group probe is enabled"
}
$groupObserved = ($inputs.group_probe.chatroom_branch_observed -eq $true) -and
((Test-NonEmptyString $inputs.group_probe.speech_permission_state) -or (Test-NonEmptyString $inputs.group_probe.upload_permission_state))
if (-not $groupObserved -and -not (Test-StructuredFailure $inputs.group_probe.failure)) {
Add-ValidationFailure $validationFailures "GROUP_OBSERVATION_OR_FAILURE_MISSING" "group_probe needs ChatRoom/permission evidence or structured failure"
}
}
$fallbackNeeded = ($inputs.fallback_a_route.enabled_if_b_route_fails -eq $true) -and (Test-StructuredFailure $inputs.runtime_observations.failure)
if ($fallbackNeeded) {
$fallbackObserved = (Test-NonEmptyString $inputs.fallback_a_route.hwnd) -and
($inputs.fallback_a_route.rtbSendMessage_present -eq $true) -and
($inputs.fallback_a_route.btnSend_present -eq $true)
if (-not $fallbackObserved -and -not (Test-StructuredFailure $inputs.fallback_a_route.failure)) {
Add-ValidationFailure $validationFailures "FALLBACK_A_ROUTE_EVIDENCE_MISSING" "fallback_a_route needs hwnd/rtbSendMessage/btnSend evidence or structured failure when B-route fails"
}
}
$beforeRuntime = Read-ProbeJson -Stage "before" -Name "probe_client_runtime.json"
$beforePreflight = Read-ProbeJson -Stage "before" -Name "send_entrypoints_preflight.json"
$beforeNetwork = Read-ProbeJson -Stage "before" -Name "network_inventory.json"
$beforeFeasibility = Read-ProbeJson -Stage "before" -Name "feasibility_summary.json"
$entrypoints = Get-EntrypointMap $beforePreflight
$runtimeSummary = [ordered]@{
source = "return-evidence/before/probe_client_runtime.json"
target_count = if ($null -ne $beforeRuntime) { $beforeRuntime.target_count } else { $null }
host = if ($null -ne $beforeRuntime) { $beforeRuntime.host } else { $null }
targets = if ($null -ne $beforeRuntime) { $beforeRuntime.targets } else { @() }
feasibility = $beforeFeasibility
}
$failures = New-Object System.Collections.Generic.List[object]
foreach ($failure in $validationFailures) { $failures.Add($failure) | Out-Null }
if ($textEnabled -and $inputs.text_attempt.success_ack_or_sent_record_present -ne $true -and (Test-NonEmptyString $inputs.text_attempt.failure.code)) {
$failures.Add($inputs.text_attempt.failure) | Out-Null
}
if ($fileEnabled -and $inputs.file_attempt.file_upload_or_sent_record_present -ne $true -and (Test-NonEmptyString $inputs.file_attempt.failure.code)) {
$failures.Add($inputs.file_attempt.failure) | Out-Null
}
if (Test-StructuredFailure $inputs.runtime_observations.failure) {
$failures.Add($inputs.runtime_observations.failure) | Out-Null
}
if ($groupEnabled -and (Test-StructuredFailure $inputs.group_probe.failure)) {
$failures.Add($inputs.group_probe.failure) | Out-Null
}
if ($fallbackNeeded -and (Test-StructuredFailure $inputs.fallback_a_route.failure)) {
$failures.Add($inputs.fallback_a_route.failure) | Out-Null
}
$output = [ordered]@{
run = [ordered]@{
schema_version = $inputs.schema_version
generated_at_local = (Get-Date).ToString("o")
machine = $env:COMPUTERNAME
operator_confirmed_logged_in = $inputs.run.operator_confirmed_logged_in
operator_started_at_local = $inputs.run.operator_started_at_local
operator_finished_at_local = $inputs.run.operator_finished_at_local
one_shot_text_enabled = $textEnabled
one_shot_file_enabled = $fileEnabled
production_send_enabled = $false
production_file_send_enabled = $false
validation_ok = ($validationFailures.Count -eq 0)
}
targets = $inputs.targets
runtime = $runtimeSummary
reachability = [ordered]@{
in_process_adapter = $inputs.adapter_reachability
operator_runtime_observations = $inputs.runtime_observations
entrypoints = $entrypoints
app_context_manager = if ($entrypoints.Contains("text_high_level_by_jid")) { $entrypoints["text_high_level_by_jid"] } else { $null }
message_scheduling = if ($entrypoints.Contains("text_message_scheduler")) { $entrypoints["text_message_scheduler"] } else { $null }
chat_xmpp = if ($entrypoints.Contains("text_wire_chat_send")) { $entrypoints["text_wire_chat_send"] } else { $null }
file_upload = if ($entrypoints.Contains("file_service_upload")) { $entrypoints["file_service_upload"] } else { $null }
file_message = if ($entrypoints.Contains("file_wire_message")) { $entrypoints["file_wire_message"] } else { $null }
group_probe = $inputs.group_probe
fallback_a_route = $inputs.fallback_a_route
}
server_auth = [ordered]@{
operator_supplied = $inputs.server_auth
network_inventory_source = "return-evidence/before/network_inventory.json"
network = $beforeNetwork
}
snapshots = [ordered]@{
sent_record_source = $inputs.sent_record_source
before_dir = "return-evidence/before"
after_text_dir = if ($textEnabled) { "return-evidence/after-text" } else { $null }
after_file_dir = if ($fileEnabled) { "return-evidence/after-file" } else { $null }
}
attempts = [ordered]@{
text = $inputs.text_attempt
file = $inputs.file_attempt
group = $inputs.group_probe
fallback_a_route = $inputs.fallback_a_route
}
failures = $failures
correlation = [ordered]@{
text_content_sha256 = $inputs.text_attempt.content_sha256
file_sha256 = $inputs.file_attempt.file_sha256
ack_or_ref_id = $inputs.sent_record_source.ack_or_ref_id
sent_record_correlation_notes = $inputs.sent_record_source.correlation_notes
text_machine_verifiable = $textEnabled -and ($inputs.text_attempt.success_ack_or_sent_record_present -eq $true) -and (Test-NonEmptyString $inputs.sent_record_source.ack_or_ref_id)
file_machine_verifiable = $fileEnabled -and ($inputs.file_attempt.file_upload_or_sent_record_present -eq $true) -and (Test-NonEmptyString $inputs.sent_record_source.ack_or_ref_id)
}
}
$output | ConvertTo-Json -Depth 16 | Set-Content -LiteralPath $outputPath -Encoding UTF8
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
$zip = Join-Path $root ("isphere-login-reachability-return-" + $stamp + ".zip")
if (Test-Path -LiteralPath $zip) {
Remove-Item -LiteralPath $zip -Force
}
$items = @(
$inputsPath,
$outputPath,
$evidenceRoot,
(Join-Path $root "EXPECTED-RETURN-FILES.txt"),
(Join-Path $root "OPERATOR-STEPS.md"),
(Join-Path $root "TODO-COVERAGE.md")
)
Compress-Archive -Path $items -DestinationPath $zip -Force
[ordered]@{
ok = ($validationFailures.Count -eq 0)
zip = $zip
output_json = $outputPath
validation_failure_count = $validationFailures.Count
production_send_enabled = $false
production_file_send_enabled = $false
} | ConvertTo-Json -Depth 8 -Compress
if ($validationFailures.Count -gt 0) {
exit 2
}
'@ | Set-Content -LiteralPath (Join-Path $packageDir "CREATE-RETURN-ZIP.ps1") -Encoding UTF8
@'
@echo off
setlocal
cd /d "%~dp0"
echo Optional local capability smoke. This does not log in and does not send.
tools\isphere-capability-smoke.exe > return-evidence\capability-smoke.txt 2>&1
echo exit code: %ERRORLEVEL% > return-evidence\capability-smoke-exit-code.txt
type return-evidence\capability-smoke-exit-code.txt
pause
'@ | Set-Content -LiteralPath (Join-Path $packageDir "optional-run-capability-smoke.bat") -Encoding ASCII
@'
{
"package": "login-reachability-probe",
"schema_version": "2026-07-13.login-reachability.v1",
"LOCAL_LOGIN_UNAVAILABLE": true,
"ONLINE_LOGIN_REQUIRED": true,
"NO_AUTOMATIC_SEND": true,
"entrypoint": "RUN-LOGIN-REACHABILITY-SUITE.bat",
"input_template": "LOGIN-REACHABILITY-INPUTS.template.json",
"return_zip_script": "CREATE-RETURN-ZIP.ps1",
"todo_coverage": "TODO-COVERAGE.md",
"recorder_exe": "recorder/ISphereLiveProbeRecorder.exe",
"capability_smoke_exe": "tools/isphere-capability-smoke.exe",
"required_output_schema_sections": [
"run",
"targets",
"runtime",
"reachability",
"server_auth",
"snapshots",
"attempts",
"failures",
"correlation"
],
"production_send_enabled": false,
"production_file_send_enabled": false,
"remaining_todo_completed": [
"runtime_observations",
"group_probe",
"fallback_a_route"
],
"a_route_rpa": "backup_only"
}
'@ | Set-Content -LiteralPath (Join-Path $packageDir "LOGIN-REACHABILITY-PACKAGE-MANIFEST.json") -Encoding UTF8
$topReadme = Join-Path $packageDir "README.txt"
if (-not (Test-Path -LiteralPath $topReadme)) {
@(
"iSphere Login Reachability Probe Package",
"",
"LOCAL_LOGIN_UNAVAILABLE: development machine cannot log in to iSphere.",
"ONLINE_LOGIN_REQUIRED: run only where iSphere/IMPlatformClient is logged in.",
"NO_AUTOMATIC_SEND: this package does not auto-send text or files.",
"",
"Entrypoint: RUN-LOGIN-REACHABILITY-SUITE.bat",
"Input: LOGIN-REACHABILITY-INPUTS.json",
"Output: LOGIN-REACHABILITY-OUTPUT.json and isphere-login-reachability-return-*.zip",
"",
"Output schema sections: run, targets, runtime, reachability, server_auth, snapshots, attempts, failures, correlation.",
"",
"Rules: one-shot text/file only; duplicate_second_send_attempted=false; duplicate_second_file_send_attempted=false; production_send_enabled=false; production_file_send_enabled=false; A-route/RPA backup_only."
) | Set-Content -LiteralPath $topReadme -Encoding UTF8
}
if (-not $NoZip) {
Compress-Archive -Path (Join-Path $packageDir "*") -DestinationPath $zipPath -Force
}
$downloadZip = $null
if ($CopyToDownloads -and (Test-Path -LiteralPath $zipPath)) {
$downloads = Join-Path $env:USERPROFILE "Downloads"
if (Test-Path -LiteralPath $downloads) {
$downloadZip = Join-Path $downloads ("login-reachability-probe-package-" + (Get-Date -Format "yyyyMMdd-HHmmss") + ".zip")
Copy-Item -LiteralPath $zipPath -Destination $downloadZip -Force
}
}
[ordered]@{
ok = $true
package_dir = (Resolve-Path -LiteralPath $packageDir).Path
zip = if (Test-Path -LiteralPath $zipPath) { (Resolve-Path -LiteralPath $zipPath).Path } else { $null }
downloads_zip = if ($downloadZip) { (Resolve-Path -LiteralPath $downloadZip).Path } else { $null }
recorder_exe = (Resolve-Path -LiteralPath (Join-Path $recorderDir "ISphereLiveProbeRecorder.exe")).Path
capability_smoke_exe = (Resolve-Path -LiteralPath $capabilityExe).Path
online_login_required = $true
no_automatic_send = $true
production_send_enabled = $false
production_file_send_enabled = $false
} | ConvertTo-Json -Depth 6 -Compress