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 顶层 schema:run/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