@hope_phenom/dsh-plugin-remote-use 0.0.0-stage → 0.2.24
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/LICENSE +21 -0
- package/README.md +207 -2
- package/README.zh.md +195 -0
- package/assets/icon.svg +47 -0
- package/cordis.patch.yml +41 -0
- package/lib/bridge/acp-bridge.js +905 -0
- package/lib/bridge/peer-connection.js +730 -0
- package/lib/client.js +2842 -0
- package/lib/core/acp-model-overlay.js +121 -0
- package/lib/core/approval-queue.js +216 -0
- package/lib/core/audit-log.js +322 -0
- package/lib/core/desktop-notify.js +173 -0
- package/lib/core/environment-prompt.js +76 -0
- package/lib/core/relay-supervisor.js +278 -0
- package/lib/core/review-engine.js +679 -0
- package/lib/core/sandbox-diagnosis.js +258 -0
- package/lib/core/sandbox-probe.js +310 -0
- package/lib/core/self-acp-spec.js +117 -0
- package/lib/core/selfcheck-runner.js +150 -0
- package/lib/core/settings-store.js +226 -0
- package/lib/core/shot-gc.js +334 -0
- package/lib/core/task-cwd.js +50 -0
- package/lib/core/task-memory.js +163 -0
- package/lib/core/task-registry.js +238 -0
- package/lib/core/toolcall-cache.js +206 -0
- package/lib/core/transfer-queue.js +58 -0
- package/lib/core/transfer.js +1144 -0
- package/lib/core/worker-arming.js +334 -0
- package/lib/index.js +2207 -0
- package/lib/provider/out-of-process-helpers.js +76 -0
- package/lib/provider/remote-provider.js +261 -0
- package/lib/tools/mediator-shim.mjs +180 -0
- package/lib/tools/prepare-sandbox-mediator.mjs +529 -0
- package/lib/tools/remote-tools.js +680 -0
- package/lib/tools/selfcheck-fix-prompt.md +206 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +72 -4
- package/slave/acp-profile-approval.patch.yml +66 -0
- package/slave/file-transfer.mjs +552 -0
- package/slave/integrity-probe.ps1 +99 -0
- package/slave/make-launch-spec.mjs +184 -0
- package/slave/net-address.mjs +138 -0
- package/slave/node_modules/.package-lock.json +26 -0
- package/slave/node_modules/ws/LICENSE +20 -0
- package/slave/node_modules/ws/README.md +548 -0
- package/slave/node_modules/ws/browser.js +8 -0
- package/slave/node_modules/ws/index.js +22 -0
- package/slave/node_modules/ws/lib/buffer-util.js +131 -0
- package/slave/node_modules/ws/lib/constants.js +19 -0
- package/slave/node_modules/ws/lib/event-target.js +292 -0
- package/slave/node_modules/ws/lib/extension.js +203 -0
- package/slave/node_modules/ws/lib/limiter.js +55 -0
- package/slave/node_modules/ws/lib/permessage-deflate.js +530 -0
- package/slave/node_modules/ws/lib/receiver.js +743 -0
- package/slave/node_modules/ws/lib/sender.js +607 -0
- package/slave/node_modules/ws/lib/stream.js +161 -0
- package/slave/node_modules/ws/lib/subprotocol.js +62 -0
- package/slave/node_modules/ws/lib/validation.js +152 -0
- package/slave/node_modules/ws/lib/websocket-server.js +562 -0
- package/slave/node_modules/ws/lib/websocket.js +1421 -0
- package/slave/node_modules/ws/package.json +74 -0
- package/slave/node_modules/ws/wrapper.mjs +21 -0
- package/slave/pairing.mjs +175 -0
- package/slave/permission-pairing.mjs +240 -0
- package/slave/prune-shots.mjs +170 -0
- package/slave/relay.mjs +1351 -0
- package/slave/repair-workspace-integrity.ps1 +75 -0
- package/slave/screenshot.ps1 +114 -0
- package/slave/selfcheck.ps1 +350 -0
- package/slave/stage-app.ps1 +116 -0
- package/slave/workspace-integrity.mjs +157 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# repair-workspace-integrity.ps1 -- reset a workspace's stale Low mandatory label to Medium.
|
|
2
|
+
#
|
|
3
|
+
# WHY: DSH's workspace-write sandbox labels the workspace Low. The label survives the session and is
|
|
4
|
+
# inherited by files copied into the tree, and Windows launches a Low-labelled image at Low
|
|
5
|
+
# integrity. Under danger-full-access that leftover is what makes a GUI app unable to write
|
|
6
|
+
# %LOCALAPPDATA% even though the agent shell is High (PITFALLS F41).
|
|
7
|
+
#
|
|
8
|
+
# The relay repairs the tree automatically on start when it runs at danger-full-access with an
|
|
9
|
+
# explicit --task-dir. This script is the manual equivalent, and the repair path for a hand-managed
|
|
10
|
+
# relay whose task directory is not passed as a flag.
|
|
11
|
+
#
|
|
12
|
+
# ASCII-only, param() first: Windows PowerShell 5.1 reads BOM-less files as ANSI.
|
|
13
|
+
param(
|
|
14
|
+
[Parameter(Mandatory = $true)][string]$Path,
|
|
15
|
+
[switch]$WhatIf
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
$ErrorActionPreference = 'Stop'
|
|
19
|
+
|
|
20
|
+
if (-not (Test-Path -LiteralPath $Path -PathType Container)) {
|
|
21
|
+
Write-Host "REFUSING: not a directory: $Path" -ForegroundColor Red
|
|
22
|
+
exit 2
|
|
23
|
+
}
|
|
24
|
+
$full = (Resolve-Path -LiteralPath $Path).Path
|
|
25
|
+
if ($full -match '^[A-Za-z]:\\?$') {
|
|
26
|
+
Write-Host "REFUSING: $full is a drive root, not a workspace." -ForegroundColor Red
|
|
27
|
+
exit 2
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
$protected = @($env:SystemRoot, $env:ProgramFiles, ${env:ProgramFiles(x86)}, $env:ProgramData) |
|
|
31
|
+
Where-Object { $_ -and $_.Trim() -ne '' } |
|
|
32
|
+
ForEach-Object { $_.TrimEnd('\').ToLowerInvariant() }
|
|
33
|
+
$target = $full.TrimEnd('\').ToLowerInvariant()
|
|
34
|
+
foreach ($root in $protected) {
|
|
35
|
+
if ($target -eq $root -or $target.StartsWith($root + '\')) {
|
|
36
|
+
Write-Host "REFUSING: $full is a protected system location." -ForegroundColor Red
|
|
37
|
+
exit 2
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
$command = "icacls `"$full`" /setintegritylevel (OI)(CI)Medium /T /C /Q"
|
|
42
|
+
Write-Host "workspace : $full"
|
|
43
|
+
Write-Host "command : $command"
|
|
44
|
+
if ($WhatIf) {
|
|
45
|
+
Write-Host 'WHATIF: nothing changed.'
|
|
46
|
+
exit 0
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Show the label lines this tree carries BEFORE the reset, so the operator can see whether there was
|
|
50
|
+
# anything to fix. The label name is localized on some Windows installs, so the decision is still the
|
|
51
|
+
# unconditional idempotent set below rather than a text match.
|
|
52
|
+
Write-Host '--- labels before (matching lines only) ---'
|
|
53
|
+
$before = @(& icacls $full 2>&1)
|
|
54
|
+
@($before | Where-Object { $_ -match 'Mandatory|Label|S-1-16' }) | ForEach-Object { Write-Host $_ }
|
|
55
|
+
|
|
56
|
+
& icacls $full /setintegritylevel '(OI)(CI)Medium' /T /C /Q | Out-Null
|
|
57
|
+
if ($LASTEXITCODE -ne 0) {
|
|
58
|
+
Write-Host "FAILED: icacls exited $LASTEXITCODE" -ForegroundColor Red
|
|
59
|
+
exit 1
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
Write-Host '--- labels after (top level) ---'
|
|
63
|
+
$after = @(& icacls $full 2>&1)
|
|
64
|
+
@($after | Where-Object { $_ -match 'Mandatory|Label|S-1-16' }) | ForEach-Object { Write-Host $_ }
|
|
65
|
+
|
|
66
|
+
# Re-measure with the same probe the self-check uses. This is the difference between "the command
|
|
67
|
+
# returned 0" and "a process launched from here is no longer Low".
|
|
68
|
+
$probe = Join-Path $PSScriptRoot 'integrity-probe.ps1'
|
|
69
|
+
if (Test-Path -LiteralPath $probe) {
|
|
70
|
+
Write-Host '--- probe ---'
|
|
71
|
+
& $probe -Workspace $full
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
Write-Host 'RESULT: OK -- the workspace tree was relabeled to Medium.' -ForegroundColor Green
|
|
75
|
+
exit 0
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# screenshot.ps1 -- capture the worker's desktop.
|
|
2
|
+
#
|
|
3
|
+
# MUST RUN IN THE INTERACTIVE SESSION. In session 0 (SSH/services) there is no desktop,
|
|
4
|
+
# `CopyFromScreen` throws or returns black, and `VirtualScreen` reports a stub size -- the
|
|
5
|
+
# same session-0 false negative documented for the display checks.
|
|
6
|
+
#
|
|
7
|
+
# ASCII-ONLY: Windows PowerShell 5.1 reads a BOM-less file as ANSI, so a single non-ASCII
|
|
8
|
+
# byte corrupts parsing (see the note in relay-ctl.ps1).
|
|
9
|
+
#
|
|
10
|
+
# The geometry report is written NEXT TO THIS SCRIPT ($PSScriptRoot), because the relay
|
|
11
|
+
# reads it from the script's own directory. Writing it to a fixed absolute path is exactly
|
|
12
|
+
# the mismatch that once made the self-check look like it produced no report, and it would
|
|
13
|
+
# silently zero the screenshot's geometry metadata.
|
|
14
|
+
#
|
|
15
|
+
# Usage: screenshot.ps1 -Out C:\path\shot.png
|
|
16
|
+
param([Parameter(Mandatory = $true)][string]$Out)
|
|
17
|
+
|
|
18
|
+
$ErrorActionPreference = 'Stop'
|
|
19
|
+
if ([string]::IsNullOrEmpty($PSScriptRoot)) { $PSScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path }
|
|
20
|
+
$report = Join-Path $PSScriptRoot 'screenshot-report.txt'
|
|
21
|
+
# Explicit UTF-8 rather than Tee-Object: in Windows PowerShell 5.1 Tee-Object writes
|
|
22
|
+
# UTF-16LE, and a UTF-8 reader then sees NUL bytes between characters so every prefix match
|
|
23
|
+
# fails -- the relay silently reported 0x0 geometry because of exactly that. The reader in
|
|
24
|
+
# relay.mjs also tolerates UTF-16 now, but the writer must not depend on it.
|
|
25
|
+
function W($m) {
|
|
26
|
+
Write-Output $m
|
|
27
|
+
Add-Content -Path $report -Value $m -Encoding UTF8
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
Remove-Item $report -Force -ErrorAction SilentlyContinue
|
|
31
|
+
Remove-Item $Out -Force -ErrorAction SilentlyContinue
|
|
32
|
+
|
|
33
|
+
W "session=$((Get-Process -Id $PID).SessionId) interactive=$([Environment]::UserInteractive) user=$env:USERNAME"
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Hide our own console BEFORE capturing.
|
|
37
|
+
#
|
|
38
|
+
# This is not redundant with the launcher's `-WindowStyle Hidden` / `windowsHide: true`:
|
|
39
|
+
# in practice a console window still reached the desktop and was photographed into the
|
|
40
|
+
# frame (an empty one, because this script's output goes to a pipe). Hiding the window we
|
|
41
|
+
# actually own is deterministic and independent of how we were started.
|
|
42
|
+
#
|
|
43
|
+
# It also removes a live focus hazard: a visible console can take foreground, which would
|
|
44
|
+
# corrupt the very GUI measurements a screenshot exists to support.
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
Add-Type @'
|
|
47
|
+
using System;
|
|
48
|
+
using System.Runtime.InteropServices;
|
|
49
|
+
public static class ConsoleHider {
|
|
50
|
+
[DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow();
|
|
51
|
+
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
|
52
|
+
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
|
|
53
|
+
public static bool Hide() {
|
|
54
|
+
IntPtr h = GetConsoleWindow();
|
|
55
|
+
if (h == IntPtr.Zero) return false; // launched with CREATE_NO_WINDOW
|
|
56
|
+
ShowWindow(h, 0); // SW_HIDE
|
|
57
|
+
return !IsWindowVisible(h);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
'@ -ErrorAction SilentlyContinue
|
|
61
|
+
$hidden = $false
|
|
62
|
+
try { $hidden = [ConsoleHider]::Hide() } catch { }
|
|
63
|
+
W "console hidden=$hidden"
|
|
64
|
+
|
|
65
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
66
|
+
Add-Type -AssemblyName System.Drawing
|
|
67
|
+
|
|
68
|
+
# Report the layout first: a useful screenshot is only meaningful next to the geometry it
|
|
69
|
+
# was taken from, and this is also how a wrong-session capture gets caught.
|
|
70
|
+
$vs = [System.Windows.Forms.SystemInformation]::VirtualScreen
|
|
71
|
+
W "virtualscreen x=$($vs.X) y=$($vs.Y) w=$($vs.Width) h=$($vs.Height)"
|
|
72
|
+
foreach ($s in [System.Windows.Forms.Screen]::AllScreens) {
|
|
73
|
+
W ("screen {0} primary={1} bounds={2} bpp={3}" -f $s.DeviceName, $s.Primary, $s.Bounds, $s.BitsPerPixel)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if ($vs.Width -le 0 -or $vs.Height -le 0) {
|
|
77
|
+
W "FAIL: virtual screen is empty -- almost certainly session 0"
|
|
78
|
+
W "RESULT: FAILED"
|
|
79
|
+
exit 1
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
$bmp = New-Object System.Drawing.Bitmap($vs.Width, $vs.Height)
|
|
83
|
+
$gfx = [System.Drawing.Graphics]::FromImage($bmp)
|
|
84
|
+
try {
|
|
85
|
+
$gfx.CopyFromScreen($vs.Left, $vs.Top, 0, 0, $bmp.Size)
|
|
86
|
+
} finally {
|
|
87
|
+
$gfx.Dispose()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# PNG, not JPEG: the point is often to read UI text back.
|
|
91
|
+
$bmp.Save($Out, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
92
|
+
$bmp.Dispose()
|
|
93
|
+
|
|
94
|
+
if (-not (Test-Path $Out)) { W "FAIL: no file written"; W "RESULT: FAILED"; exit 1 }
|
|
95
|
+
$len = (Get-Item $Out).Length
|
|
96
|
+
W "wrote $Out ($len bytes)"
|
|
97
|
+
|
|
98
|
+
# A pure-black frame is the classic session-0 / no-desktop symptom, so check rather than
|
|
99
|
+
# hand back something that looks like a successful capture.
|
|
100
|
+
$probe = New-Object System.Drawing.Bitmap($Out)
|
|
101
|
+
try {
|
|
102
|
+
$distinct = @{}
|
|
103
|
+
for ($x = 0; $x -lt $probe.Width; $x += [Math]::Max(1, [int]($probe.Width / 40))) {
|
|
104
|
+
for ($y = 0; $y -lt $probe.Height; $y += [Math]::Max(1, [int]($probe.Height / 30))) {
|
|
105
|
+
$distinct[$probe.GetPixel($x, $y).ToArgb()] = $true
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
W "distinct sampled colors: $($distinct.Count)"
|
|
109
|
+
if ($distinct.Count -le 1) { W "WARN: the frame is a single flat color -- capture may be blank" }
|
|
110
|
+
} finally {
|
|
111
|
+
$probe.Dispose()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
W "RESULT: OK"
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# slave-selfcheck.ps1 -- DESIGN.md section 5.10 self-check.
|
|
2
|
+
# ASCII-ONLY ON PURPOSE: Windows PowerShell 5.1 reads BOM-less files as ANSI, and a
|
|
3
|
+
# non-ASCII byte sequence inside a string literal breaks parsing.
|
|
4
|
+
# Dual mode: SYSTEM (boot) checks the system layer; interactive (logon) adds S7/S8.
|
|
5
|
+
#
|
|
6
|
+
# Output goes NEXT TO THIS SCRIPT ($PSScriptRoot), not to a fixed absolute path: the
|
|
7
|
+
# relay reads the JSON from the script's own directory, so keeping them colocated makes
|
|
8
|
+
# the pair relocatable and removes a silent mismatch when either is moved.
|
|
9
|
+
#
|
|
10
|
+
# `param()` must be the FIRST statement after the comments: PowerShell rejects it after any
|
|
11
|
+
# assignment, and PS 5.1 reports that as a confusing "invalid assignment operator" further down.
|
|
12
|
+
param(
|
|
13
|
+
# The GUI application under test, for S18. Empty means "this worker does not verify a specific
|
|
14
|
+
# app", which is reported as unconfigured rather than missing.
|
|
15
|
+
[string]$App = '',
|
|
16
|
+
# The task directory, for S20. The relay passes the directory it actually hands the agent, so the
|
|
17
|
+
# integrity probe measures the tree remote work really runs in. Empty means no probe.
|
|
18
|
+
[string]$Workspace = ''
|
|
19
|
+
)
|
|
20
|
+
$ErrorActionPreference = 'Continue'
|
|
21
|
+
if ([string]::IsNullOrEmpty($PSScriptRoot)) { $PSScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path }
|
|
22
|
+
$outFile = Join-Path $PSScriptRoot 'selfcheck.json'
|
|
23
|
+
$txtFile = Join-Path $PSScriptRoot 'selfcheck.txt'
|
|
24
|
+
|
|
25
|
+
Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue
|
|
26
|
+
Add-Type @'
|
|
27
|
+
using System;
|
|
28
|
+
using System.Text;
|
|
29
|
+
using System.Runtime.InteropServices;
|
|
30
|
+
public class SC {
|
|
31
|
+
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
|
|
32
|
+
public struct DD {
|
|
33
|
+
public int cb;
|
|
34
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)] public string DeviceName;
|
|
35
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] public string DeviceString;
|
|
36
|
+
public int StateFlags;
|
|
37
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] public string DeviceID;
|
|
38
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] public string DeviceKey;
|
|
39
|
+
}
|
|
40
|
+
[DllImport("user32.dll", CharSet=CharSet.Unicode)]
|
|
41
|
+
public static extern bool EnumDisplayDevices(string dev, uint n, ref DD d, uint flags);
|
|
42
|
+
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
|
|
43
|
+
public struct DEVMODE {
|
|
44
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)] public string dmDeviceName;
|
|
45
|
+
public short dmSpecVersion, dmDriverVersion, dmSize, dmDriverExtra;
|
|
46
|
+
public int dmFields, dmPositionX, dmPositionY, dmDisplayOrientation, dmDisplayFixedOutput;
|
|
47
|
+
public short dmColor, dmDuplex, dmYResolution, dmTTOption, dmCollate;
|
|
48
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)] public string dmFormName;
|
|
49
|
+
public short dmLogPixels;
|
|
50
|
+
public int dmBitsPerPel, dmPelsWidth, dmPelsHeight, dmDisplayFlags, dmDisplayFrequency;
|
|
51
|
+
public int dmICMMethod, dmICMIntent, dmMediaType, dmDitherType, dmReserved1, dmReserved2, dmPanningWidth, dmPanningHeight;
|
|
52
|
+
}
|
|
53
|
+
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern bool EnumDisplaySettings(string d,int m,ref DEVMODE dm);
|
|
54
|
+
[StructLayout(LayoutKind.Sequential)] public struct POINT { public int X,Y; }
|
|
55
|
+
[DllImport("user32.dll")] public static extern bool SetCursorPos(int x,int y);
|
|
56
|
+
[DllImport("user32.dll")] public static extern bool GetCursorPos(out POINT p);
|
|
57
|
+
[DllImport("user32.dll")] public static extern IntPtr OpenInputDesktop(uint f,bool i,uint a);
|
|
58
|
+
[DllImport("user32.dll")] public static extern bool CloseDesktop(IntPtr h);
|
|
59
|
+
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern bool GetUserObjectInformation(IntPtr h,int i,StringBuilder p,int n,out int need);
|
|
60
|
+
[DllImport("kernel32.dll")] public static extern uint WTSGetActiveConsoleSessionId();
|
|
61
|
+
|
|
62
|
+
public static string[] Adapters() {
|
|
63
|
+
var l = new System.Collections.Generic.List<string>();
|
|
64
|
+
var d = new DD(); d.cb = Marshal.SizeOf(typeof(DD)); uint i = 0;
|
|
65
|
+
while (EnumDisplayDevices(null, i, ref d, 0)) {
|
|
66
|
+
var mons = new System.Collections.Generic.List<string>();
|
|
67
|
+
var m = new DD(); m.cb = Marshal.SizeOf(typeof(DD)); uint j = 0;
|
|
68
|
+
while (EnumDisplayDevices(d.DeviceName, j, ref m, 0)) {
|
|
69
|
+
mons.Add(m.DeviceString + " flags=" + m.StateFlags);
|
|
70
|
+
m = new DD(); m.cb = Marshal.SizeOf(typeof(DD)); j++;
|
|
71
|
+
}
|
|
72
|
+
l.Add(d.DeviceName + " | " + d.DeviceString + " | flags=" + d.StateFlags + " | mons=" + mons.Count + " {" + string.Join(", ", mons) + "}");
|
|
73
|
+
d = new DD(); d.cb = Marshal.SizeOf(typeof(DD)); i++;
|
|
74
|
+
}
|
|
75
|
+
return l.ToArray();
|
|
76
|
+
}
|
|
77
|
+
public static string InputDesktop() {
|
|
78
|
+
IntPtr h = OpenInputDesktop(0, false, 0x0001);
|
|
79
|
+
if (h == IntPtr.Zero) return "<err " + Marshal.GetLastWin32Error() + ">";
|
|
80
|
+
try { var sb = new StringBuilder(256); int n; return GetUserObjectInformation(h, 2, sb, 512, out n) ? sb.ToString() : "?"; }
|
|
81
|
+
finally { CloseDesktop(h); }
|
|
82
|
+
}
|
|
83
|
+
public static string CursorTest(int x, int y) {
|
|
84
|
+
SetCursorPos(x - 3, y - 3); bool ok = SetCursorPos(x, y);
|
|
85
|
+
var p = new POINT(); GetCursorPos(out p);
|
|
86
|
+
return (p.X == x && p.Y == y) ? "ok" : ("FAIL set=" + ok + " got=" + p.X + "," + p.Y);
|
|
87
|
+
}
|
|
88
|
+
public static string ConsoleId() { return WTSGetActiveConsoleSessionId().ToString(); }
|
|
89
|
+
}
|
|
90
|
+
'@ -ErrorAction SilentlyContinue
|
|
91
|
+
|
|
92
|
+
$checks = New-Object System.Collections.ArrayList
|
|
93
|
+
function Add-Check($id, $name, $sev, $status, $expected, $actual, $fix) {
|
|
94
|
+
[void]$checks.Add([ordered]@{
|
|
95
|
+
id = $id; name = $name; severity = $sev; status = $status
|
|
96
|
+
expected = $expected; actual = $actual; fix = $fix
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
$sid = (Get-Process -Id $PID).SessionId
|
|
101
|
+
$interactive = [Environment]::UserInteractive
|
|
102
|
+
$adaps = @()
|
|
103
|
+
try { $adaps = [SC]::Adapters() } catch {}
|
|
104
|
+
$screens = @()
|
|
105
|
+
try { $screens = @([System.Windows.Forms.Screen]::AllScreens) } catch {}
|
|
106
|
+
$primary = $screens | Where-Object { $_.Primary } | Select-Object -First 1
|
|
107
|
+
$vddAdap = @($adaps | Where-Object { $_ -match 'Virtual Display|MttVDD|VDD' })
|
|
108
|
+
$edids = @()
|
|
109
|
+
try { $edids = @(Get-CimInstance -Namespace root\wmi -ClassName WmiMonitorID -ErrorAction SilentlyContinue) } catch {}
|
|
110
|
+
$edidNames = @($edids | ForEach-Object { -join ($_.UserFriendlyName | Where-Object { $_ -ne 0 } | ForEach-Object { [char]$_ }) })
|
|
111
|
+
|
|
112
|
+
# S1 display device attached to the desktop
|
|
113
|
+
Add-Check 'S1' 'display adapter attached' 'blocking' $(if ($adaps.Count -gt 0) { 'pass' } else { 'fail' }) '>=1 adapter' ($adaps.Count.ToString() + ' adapters') 'install/enable a display adapter'
|
|
114
|
+
# S2 virtual display present with EDID
|
|
115
|
+
Add-Check 'S2' 'virtual display present (not WinDisc fallback)' 'blocking' $(if ($vddAdap.Count -gt 0 -and $edids.Count -gt 0) { 'pass' } else { 'fail' }) 'VDD + EDID' ('vdd=' + $vddAdap.Count + ' edid=[' + ($edidNames -join ',') + ']') 'install/enable Virtual Display Driver'
|
|
116
|
+
# S3 resolution
|
|
117
|
+
$res = if ($primary) { "$($primary.Bounds.Width)x$($primary.Bounds.Height)" } else { '?' }
|
|
118
|
+
Add-Check 'S3' 'primary resolution' 'warning' $(if ($res -eq '1920x1080') { 'pass' } else { 'warn' }) '1920x1080' $res 'put the target resolution first in vdd_settings.xml'
|
|
119
|
+
# S4 primary at origin -- AND the report says whether that primary is the virtual display
|
|
120
|
+
#
|
|
121
|
+
# WHY THIS IS NOT A BLOCKING "MUST BE VIRTUAL" CHECK, after it briefly was one:
|
|
122
|
+
#
|
|
123
|
+
# DESIGN words S4 as "the virtual screen is primary AND at (0,0)". Enforcing the first half made a
|
|
124
|
+
# real deployment unusable, and the reason is worth keeping: Windows 11 draws the LOGON UI on the
|
|
125
|
+
# PRIMARY display only. Make the virtual display primary and the login screen is rendered on a
|
|
126
|
+
# display with no physical output -- the operator's monitor stays black and they cannot log in.
|
|
127
|
+
#
|
|
128
|
+
# The requirement existed to guarantee "a coordinate space independent of the operator's monitor".
|
|
129
|
+
# On a KVM that keeps presenting the monitor's EDID, Windows never sees the display disappear, so
|
|
130
|
+
# that guarantee is already provided by the hardware -- and insisting on the virtual display only
|
|
131
|
+
# moves the logon screen somewhere nobody can see it.
|
|
132
|
+
#
|
|
133
|
+
# What still blocks is the half that protects a running task: a primary that is not at (0,0) means
|
|
134
|
+
# every absolute coordinate is silently offset. Whether the primary is the virtual display or the
|
|
135
|
+
# operator's own is reported in `actual`, so the situation is visible without being fatal.
|
|
136
|
+
$okOrigin = $false; $origin = '?'; $primaryIsVirtual = $false; $primaryAdapter = ''
|
|
137
|
+
if ($primary) {
|
|
138
|
+
$origin = "primary=$($primary.DeviceName) at $($primary.Bounds.X),$($primary.Bounds.Y)"
|
|
139
|
+
$okOrigin = ($primary.Bounds.X -eq 0 -and $primary.Bounds.Y -eq 0)
|
|
140
|
+
# `Adapters()` returns one line per adapter, each starting with its device name (`\\.\DISPLAY1`),
|
|
141
|
+
# which is exactly what `Screen.DeviceName` gives us -- so no extra P/Invoke is needed.
|
|
142
|
+
$primaryAdapter = @($adaps | Where-Object { $_ -like "$($primary.DeviceName) *" }) | Select-Object -First 1
|
|
143
|
+
if ($primaryAdapter) { $primaryIsVirtual = [bool]($primaryAdapter -match 'Virtual Display|MttVDD|VDD') }
|
|
144
|
+
}
|
|
145
|
+
$primaryDesc = $origin + ' virtual=' + $primaryIsVirtual
|
|
146
|
+
if ($primaryAdapter) { $primaryDesc = $primaryDesc + ' [' + (($primaryAdapter -split ' \| ')[1]) + ']' }
|
|
147
|
+
Add-Check 'S4' 'primary monitor at origin' 'blocking' $(if ($okOrigin) { 'pass' } else { 'fail' }) 'primary at (0,0)' $primaryDesc 'ChangeDisplaySettingsEx CDS_SET_PRIMARY|CDS_NORESET then commit'
|
|
148
|
+
# S5 DPI
|
|
149
|
+
$scale = 1.00
|
|
150
|
+
try { $g = [System.Drawing.Graphics]::FromHwnd([IntPtr]::Zero); if ($g) { $scale = [math]::Round($g.DpiX / 96.0, 2); $g.Dispose() } } catch {}
|
|
151
|
+
Add-Check 'S5' 'DPI scale' 'blocking' $(if ($scale -eq 1.0) { 'pass' } else { 'fail' }) '1.00' $scale.ToString() 'set display scaling to 100%'
|
|
152
|
+
# S6 console session
|
|
153
|
+
$q = (& query.exe session 2>&1 | Out-String)
|
|
154
|
+
$cid = [SC]::ConsoleId()
|
|
155
|
+
$isConsole = ($q -match ('(?m)^>?console\s+\S*\s*' + $cid + '\s'))
|
|
156
|
+
Add-Check 'S6' 'session bound to console' 'blocking' $(if ($isConsole) { 'pass' } else { 'fail' }) ('console id=' + $cid) ('mySession=' + $sid + ' consoleId=' + $cid) 'tscon <id> /dest:console as SYSTEM'
|
|
157
|
+
# S7 unlocked (interactive only)
|
|
158
|
+
if ($interactive) {
|
|
159
|
+
$dsk = [SC]::InputDesktop()
|
|
160
|
+
Add-Check 'S7' 'session unlocked' 'blocking' $(if ($dsk -eq 'Default') { 'pass' } else { 'fail' }) 'Default' $dsk 'unlock the session; disable auto-lock'
|
|
161
|
+
} else {
|
|
162
|
+
Add-Check 'S7' 'session unlocked' 'blocking' 'unchecked' 'Default' 'n/a in system session' 'must be checked in an interactive session'
|
|
163
|
+
}
|
|
164
|
+
# S8 live input injection (interactive only)
|
|
165
|
+
if ($interactive) {
|
|
166
|
+
$r = [SC]::CursorTest(960, 540)
|
|
167
|
+
Add-Check 'S8' 'SendInput / SetCursorPos live' 'blocking' $(if ($r -eq 'ok') { 'pass' } else { 'fail' }) 'cursor moves' $r 'requires S1+S6+S7 to pass'
|
|
168
|
+
} else {
|
|
169
|
+
Add-Check 'S8' 'SendInput / SetCursorPos live' 'blocking' 'unchecked' 'cursor moves' 'n/a in system session' 'must be checked in an interactive session'
|
|
170
|
+
}
|
|
171
|
+
# S9 no sleep
|
|
172
|
+
function PwrIdx($sub, $set) {
|
|
173
|
+
$o = & powercfg /q SCHEME_CURRENT $sub $set 2>&1 | Out-String
|
|
174
|
+
$m = [regex]::Matches($o, '0x[0-9a-fA-F]{8}')
|
|
175
|
+
if ($m.Count -ge 2) { return @([Convert]::ToInt64($m[$m.Count - 2].Value, 16), [Convert]::ToInt64($m[$m.Count - 1].Value, 16)) }
|
|
176
|
+
return @(-1, -1)
|
|
177
|
+
}
|
|
178
|
+
$standby = PwrIdx 'SUB_SLEEP' 'STANDBYIDLE'
|
|
179
|
+
$hib = PwrIdx 'SUB_SLEEP' 'HIBERNATEIDLE'
|
|
180
|
+
$noSleep = ($standby[0] -eq 0) -and ($standby[1] -eq 0) -and ($hib[0] -eq 0) -and ($hib[1] -eq 0)
|
|
181
|
+
Add-Check 'S9' 'sleep/hibernate disabled' 'blocking' $(if ($noSleep) { 'pass' } else { 'fail' }) 'AC=0 DC=0' ('standby=' + ($standby -join '/') + ' hibernate=' + ($hib -join '/')) 'powercfg /change standby-timeout-ac 0 (AC and DC)'
|
|
182
|
+
# S10 screensaver
|
|
183
|
+
$ss = (Get-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaveActive -ErrorAction SilentlyContinue).ScreenSaveActive
|
|
184
|
+
Add-Check 'S10' 'screensaver off' 'warning' $(if ("$ss" -eq '0') { 'pass' } else { 'warn' }) '0' "$ss" 'HKCU Control Panel Desktop ScreenSaveActive=0'
|
|
185
|
+
# S11 no auto-lock policy
|
|
186
|
+
$il = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name InactivityTimeoutSecs -ErrorAction SilentlyContinue).InactivityTimeoutSecs
|
|
187
|
+
Add-Check 'S11' 'no auto-lock policy' 'warning' $(if ($null -eq $il) { 'pass' } else { 'warn' }) 'not set' $(if ($null -eq $il) { 'not set' } else { "$il" }) 'remove InactivityTimeoutSecs policy'
|
|
188
|
+
# S12 QuickEdit off
|
|
189
|
+
$qe = (Get-ItemProperty 'HKCU:\Console' -Name QuickEdit -ErrorAction SilentlyContinue).QuickEdit
|
|
190
|
+
Add-Check 'S12' 'console QuickEdit off' 'blocking' $(if ("$qe" -eq '0') { 'pass' } else { 'fail' }) '0' "$qe" 'HKCU Console QuickEdit=0 and InsertMode=0'
|
|
191
|
+
# S14 DSH installed
|
|
192
|
+
#
|
|
193
|
+
# The path is DERIVED, not hardcoded -- but "derive it from $env:LOCALAPPDATA" is NOT enough, and
|
|
194
|
+
# getting that wrong once already is why this comment is long.
|
|
195
|
+
#
|
|
196
|
+
# The history: it used to be a literal path under one user's profile, correct on exactly one machine. The
|
|
197
|
+
# first fix derived it from the environment, which is correct for an interactive run and WRONG for
|
|
198
|
+
# the other mode this script has -- a boot check under a SYSTEM scheduled task, where
|
|
199
|
+
# $env:LOCALAPPDATA points at C:\Windows\system32\config\systemprofile and no candidate matches.
|
|
200
|
+
# So the naive derivation replaced a check that was right on one machine with one that is wrong on
|
|
201
|
+
# all of them in that mode.
|
|
202
|
+
#
|
|
203
|
+
# The candidates are therefore gathered from every source that can name the install:
|
|
204
|
+
# 1. a RUNNING DSH process -- whatever account we are, it says where DSH actually is
|
|
205
|
+
# 2. the current account's install location
|
|
206
|
+
# 3. Program Files
|
|
207
|
+
# 4. every user profile on the machine -- what a SYSTEM run cannot see about itself
|
|
208
|
+
$dshCandidates = New-Object System.Collections.ArrayList
|
|
209
|
+
$runningDsh = Get-Process -Name 'DeepSeek Harness' -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
210
|
+
if ($runningDsh -and $runningDsh.Path) { [void]$dshCandidates.Add((Split-Path -Parent $runningDsh.Path)) }
|
|
211
|
+
[void]$dshCandidates.Add((Join-Path $env:LOCALAPPDATA 'Programs\DeepSeek Harness'))
|
|
212
|
+
[void]$dshCandidates.Add((Join-Path $env:ProgramFiles 'DeepSeek Harness'))
|
|
213
|
+
if (${env:ProgramFiles(x86)}) { [void]$dshCandidates.Add((Join-Path ${env:ProgramFiles(x86)} 'DeepSeek Harness')) }
|
|
214
|
+
[void]$dshCandidates.Add('C:\Program Files\DeepSeek Harness')
|
|
215
|
+
$profileRoots = @()
|
|
216
|
+
if ($env:SystemDrive) { $profileRoots += (Join-Path $env:SystemDrive 'Users') }
|
|
217
|
+
foreach ($root in $profileRoots) {
|
|
218
|
+
Get-ChildItem $root -Directory -ErrorAction SilentlyContinue | ForEach-Object {
|
|
219
|
+
[void]$dshCandidates.Add((Join-Path $_.FullName 'AppData\Local\Programs\DeepSeek Harness'))
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
$dshCandidates = @($dshCandidates | Where-Object { $_ -and $_ -ne '' } | Select-Object -Unique)
|
|
223
|
+
$ver = ''
|
|
224
|
+
foreach ($candidate in $dshCandidates) {
|
|
225
|
+
if (-not (Test-Path $candidate)) { continue }
|
|
226
|
+
$exe = Get-ChildItem $candidate -Filter '*.exe' -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
227
|
+
if ($exe) { $ver = $exe.VersionInfo.FileVersion; break }
|
|
228
|
+
}
|
|
229
|
+
Add-Check 'S14' 'DSH installed' 'blocking' $(if ($ver) { 'pass' } else { 'fail' }) 'installed' $(if ($ver) { $ver } else { 'nowhere in ' + $dshCandidates.Count + ' candidate locations' }) 'install DSH and match the master version'
|
|
230
|
+
# S15 credentials
|
|
231
|
+
# Same reasoning and the same sources: a SYSTEM run must still be able to find the account whose
|
|
232
|
+
# DSH is signed in. The report names WHICH path it looked at, and lists them all when it finds
|
|
233
|
+
# none, so a genuine "signed out" is distinguishable from "looked in the wrong place".
|
|
234
|
+
$credCandidates = New-Object System.Collections.ArrayList
|
|
235
|
+
[void]$credCandidates.Add((Join-Path $env:USERPROFILE '.dsh\.credentials.yaml'))
|
|
236
|
+
[void]$credCandidates.Add((Join-Path $env:APPDATA 'dsh\.credentials.yaml'))
|
|
237
|
+
foreach ($root in $profileRoots) {
|
|
238
|
+
Get-ChildItem $root -Directory -ErrorAction SilentlyContinue | ForEach-Object {
|
|
239
|
+
[void]$credCandidates.Add((Join-Path $_.FullName '.dsh\.credentials.yaml'))
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
$credCandidates = @($credCandidates | Where-Object { $_ -and $_ -ne '' } | Select-Object -Unique)
|
|
243
|
+
$credFound = ''
|
|
244
|
+
foreach ($candidate in $credCandidates) {
|
|
245
|
+
if (Test-Path $candidate) { $credFound = $candidate; break }
|
|
246
|
+
}
|
|
247
|
+
$hasCred = $credFound -ne ''
|
|
248
|
+
Add-Check 'S15' 'model credentials present' 'blocking' $(if ($hasCred) { 'pass' } else { 'fail' }) 'exists' $(if ($hasCred) { $credFound } else { 'missing in ' + $credCandidates.Count + ' locations, including ' + $credCandidates[0] }) 'sign in to DSH on the worker'
|
|
249
|
+
# S16 disk space
|
|
250
|
+
$d = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='C:'" -ErrorAction SilentlyContinue
|
|
251
|
+
$freeGB = if ($d) { [math]::Round($d.FreeSpace / 1GB, 1) } else { 0 }
|
|
252
|
+
Add-Check 'S16' 'free disk space' 'warning' $(if ($freeGB -ge 50) { 'pass' } else { 'warn' }) '>=50 GB' "$freeGB GB" 'free up disk'
|
|
253
|
+
# S17 GPU (informational)
|
|
254
|
+
$gpu = (Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue | ForEach-Object { $_.Name + ' ' + $_.DriverVersion }) -join ' ; '
|
|
255
|
+
Add-Check 'S17' 'GPU model/driver (reported)' 'info' 'pass' 'n/a' $gpu '-'
|
|
256
|
+
# S18 target app present
|
|
257
|
+
#
|
|
258
|
+
# The path is CONFIGURABLE, and the check says which path it looked at.
|
|
259
|
+
#
|
|
260
|
+
# It used to be hardcoded to the reference worker's build output
|
|
261
|
+
# ('C:\dsh-remote\avalonia-demo\...\avalonia-demo.exe'), so on every other machine it reported
|
|
262
|
+
# 'missing' -- the same defect class as S14/S15: a check that names a specific machine's layout
|
|
263
|
+
# and then reports the absence of that layout as a fault of the machine under test. It is a
|
|
264
|
+
# `warning` rather than `blocking`, so the verdict was unaffected, but a report that warns about
|
|
265
|
+
# something that is not wrong teaches the operator to stop reading the warnings.
|
|
266
|
+
#
|
|
267
|
+
# Unconfigured is not the same as missing, so it is reported as such. Pass -App <path>, or set
|
|
268
|
+
# DSH_SELFCHECK_APP, to check for a specific application.
|
|
269
|
+
$app = if (-not [string]::IsNullOrEmpty($App)) { $App } else { [string]$env:DSH_SELFCHECK_APP }
|
|
270
|
+
if ([string]::IsNullOrEmpty($app)) {
|
|
271
|
+
Add-Check 'S18' 'target app present' 'warning' 'warn' 'configured app exists' 'not configured (pass -App or set DSH_SELFCHECK_APP)' 'set the app under test if this worker verifies one'
|
|
272
|
+
} else {
|
|
273
|
+
$appThere = Test-Path $app
|
|
274
|
+
Add-Check 'S18' 'target app present' 'warning' $(if ($appThere) { 'pass' } else { 'warn' }) 'exists' $(if ($appThere) { 'present: ' + $app } else { 'missing at: ' + $app }) 'sync the app build output'
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
# S20 workspace launch integrity
|
|
278
|
+
#
|
|
279
|
+
# WHY THIS IS IN THE READY REPORT: the failure it detects (a GUI app launched from the workspace
|
|
280
|
+
# runs at Low integrity and cannot write %LOCALAPPDATA%) presents as an application or ACL fault.
|
|
281
|
+
# The setting says danger-full-access, the agent shell is High, and only a copied executable reveals
|
|
282
|
+
# the downgrade. The probe copies a signed system binary into the workspace and into %TEMP% and
|
|
283
|
+
# compares the mandatory labels of the two child tokens.
|
|
284
|
+
#
|
|
285
|
+
# It is a warning, not a blocker: GUI work is broken, but the worker is otherwise usable and the fix
|
|
286
|
+
# is a label reset rather than a redeploy. Unconfigured workspace means no probe, not a failure.
|
|
287
|
+
# The relay only passes -Workspace under danger-full-access; a workspace-write workspace is SUPPOSED
|
|
288
|
+
# to be Low, so warning about it there would be a false alarm.
|
|
289
|
+
$integrity = $null
|
|
290
|
+
if (-not [string]::IsNullOrEmpty($Workspace)) {
|
|
291
|
+
$probeScript = Join-Path $PSScriptRoot 'integrity-probe.ps1'
|
|
292
|
+
if (-not (Test-Path -LiteralPath $probeScript)) {
|
|
293
|
+
Add-Check 'S20' 'workspace launch integrity' 'warning' 'warn' 'same as %TEMP%' ('integrity-probe.ps1 missing next to selfcheck.ps1: ' + $probeScript) 'deploy integrity-probe.ps1 with the relay, or stage GUI apps outside the workspace'
|
|
294
|
+
} else {
|
|
295
|
+
try {
|
|
296
|
+
$raw = & $probeScript -Workspace $Workspace 2>$null
|
|
297
|
+
$integrity = ($raw | Out-String) | ConvertFrom-Json
|
|
298
|
+
$fmt = { param($x) if ($null -eq $x) { '?' } else { "$($x.level)($($x.sid))" } }
|
|
299
|
+
$actual = 'self=' + (& $fmt $integrity.self) + ' workspace=' + (& $fmt $integrity.workspace) + ' temp=' + (& $fmt $integrity.temp)
|
|
300
|
+
if ($integrity.error -ne '') { $actual = $actual + ' error=' + $integrity.error }
|
|
301
|
+
if ($integrity.degraded) {
|
|
302
|
+
Add-Check 'S20' 'workspace launch integrity' 'warning' 'warn' 'workspace at least %TEMP% integrity' $actual 'A Low label inherited from an earlier workspace-write session is downgrading apps in this tree. Under danger-full-access the relay repairs it on start (--task-dir), or run slave/repair-workspace-integrity.ps1 -Path <workspace>; otherwise stage the app outside the workspace with slave/stage-app.ps1'
|
|
303
|
+
} elseif ($null -eq $integrity.workspace -or $null -eq $integrity.temp) {
|
|
304
|
+
Add-Check 'S20' 'workspace launch integrity' 'warning' 'warn' 'same as %TEMP%' $actual 'the probe could not launch from one of the two directories; check that the task directory exists and is writable by this account'
|
|
305
|
+
} else {
|
|
306
|
+
Add-Check 'S20' 'workspace launch integrity' 'warning' 'pass' 'workspace at least %TEMP% integrity' $actual ''
|
|
307
|
+
}
|
|
308
|
+
} catch {
|
|
309
|
+
Add-Check 'S20' 'workspace launch integrity' 'warning' 'warn' 'same as %TEMP%' ('probe failed: ' + $_.Exception.Message) 'run slave/integrity-probe.ps1 -Workspace <dir> by hand to see the failure'
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
$blocking = @($checks | Where-Object { $_.severity -eq 'blocking' -and $_.status -eq 'fail' })
|
|
315
|
+
$warn = @($checks | Where-Object { $_.severity -in @('blocking', 'warning') -and $_.status -eq 'warn' })
|
|
316
|
+
$unchecked = @($checks | Where-Object { $_.status -eq 'unchecked' })
|
|
317
|
+
$verdict = if ($blocking.Count -gt 0) { 'blocked' } elseif ($unchecked.Count -gt 0) { 'partial' } elseif ($warn.Count -gt 0) { 'warn' } else { 'ready' }
|
|
318
|
+
|
|
319
|
+
$payload = [ordered]@{
|
|
320
|
+
schemaVersion = 1
|
|
321
|
+
generatedAt = (Get-Date).ToString('o')
|
|
322
|
+
worker = $env:COMPUTERNAME
|
|
323
|
+
sessionId = $sid
|
|
324
|
+
interactive = $interactive
|
|
325
|
+
# The master's dispatch gate needs to tell "the worker is broken" apart from "nobody
|
|
326
|
+
# is logged in on it": the first is a fault, the second is an operational state that
|
|
327
|
+
# cannot be fixed from the master. Without this field the gate only sees blocking
|
|
328
|
+
# failures and would report a logged-out worker as a fault (DESIGN section 5.10.6).
|
|
329
|
+
sessionSegment = $(if ($interactive) { 'ok' } else { 'unavailable' })
|
|
330
|
+
verdict = $verdict
|
|
331
|
+
blockingFailures = $blocking.Count
|
|
332
|
+
warnings = $warn.Count
|
|
333
|
+
unchecked = $unchecked.Count
|
|
334
|
+
# The task directory the integrity probe measured, and its raw readings. Null when no workspace
|
|
335
|
+
# was supplied -- "not probed" must be distinguishable from "probed and equal".
|
|
336
|
+
workspace = $Workspace
|
|
337
|
+
integrity = $integrity
|
|
338
|
+
checks = $checks
|
|
339
|
+
}
|
|
340
|
+
$payload | ConvertTo-Json -Depth 6 | Set-Content -Path $outFile -Encoding UTF8
|
|
341
|
+
|
|
342
|
+
$lines = @()
|
|
343
|
+
$lines += "verdict=$verdict blocking=$($blocking.Count) warn=$($warn.Count) unchecked=$($unchecked.Count) session=$sid interactive=$interactive"
|
|
344
|
+
foreach ($c in $checks) {
|
|
345
|
+
$mark = switch ($c.status) { 'pass' { 'OK ' } 'fail' { 'FAIL' } 'warn' { 'WARN' } 'unchecked' { '?? ' } default { ' ' } }
|
|
346
|
+
$lines += (" [" + $mark + "] " + $c.id + " " + $c.name + " | expected=" + $c.expected + " | actual=" + $c.actual)
|
|
347
|
+
}
|
|
348
|
+
$lines += "json: $outFile"
|
|
349
|
+
$lines | Set-Content -Path $txtFile -Encoding UTF8
|
|
350
|
+
$lines | ForEach-Object { Write-Output $_ }
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# stage-app.ps1 -- copy a GUI app OUTSIDE the workspace before launching it.
|
|
2
|
+
#
|
|
3
|
+
# WHY: a workspace that a workspace-write session has ever used carries an inheritable Low mandatory
|
|
4
|
+
# label. Windows launches a Low-labelled image at Low integrity, and a Low process cannot write
|
|
5
|
+
# %LOCALAPPDATA% even when the account's ACL grants it. Copying the app to a normal user location
|
|
6
|
+
# (%LOCALAPPDATA%\dsh-remote-use\staging by default) removes the inherited label, which is the
|
|
7
|
+
# reliable alternative to repairing the workspace itself (PITFALLS F41).
|
|
8
|
+
#
|
|
9
|
+
# The relay repairs an explicitly armed workspace automatically under danger-full-access. This helper
|
|
10
|
+
# is for the other two cases: a hand-managed relay with no --task-dir, and an operator who prefers to
|
|
11
|
+
# keep GUI payloads out of the workspace entirely.
|
|
12
|
+
#
|
|
13
|
+
# ASCII-only, param() first: Windows PowerShell 5.1 reads BOM-less files as ANSI.
|
|
14
|
+
param(
|
|
15
|
+
[Parameter(Mandatory = $true)][string]$Source,
|
|
16
|
+
[string]$Destination = '',
|
|
17
|
+
[string]$ExeName = '',
|
|
18
|
+
[switch]$Force,
|
|
19
|
+
[switch]$Launch,
|
|
20
|
+
[string]$Arguments = ''
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
$ErrorActionPreference = 'Stop'
|
|
24
|
+
|
|
25
|
+
if (-not (Test-Path -LiteralPath $Source)) {
|
|
26
|
+
Write-Host "REFUSING: source does not exist: $Source" -ForegroundColor Red
|
|
27
|
+
exit 2
|
|
28
|
+
}
|
|
29
|
+
$sourceFull = (Resolve-Path -LiteralPath $Source).Path
|
|
30
|
+
$sourceItem = Get-Item -LiteralPath $sourceFull
|
|
31
|
+
$leaf = [System.IO.Path]::GetFileNameWithoutExtension($sourceItem.Name)
|
|
32
|
+
if ([string]::IsNullOrEmpty($leaf)) { $leaf = 'app' }
|
|
33
|
+
|
|
34
|
+
if ([string]::IsNullOrEmpty($Destination)) {
|
|
35
|
+
$stamp = (Get-Date).ToString('yyyyMMdd-HHmmss')
|
|
36
|
+
$Destination = Join-Path $env:LOCALAPPDATA ("dsh-remote-use\staging\" + $leaf + '-' + $stamp)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
$targetItem = $null
|
|
40
|
+
if ($sourceItem.PSIsContainer) {
|
|
41
|
+
$targetDir = $Destination
|
|
42
|
+
if ((Test-Path -LiteralPath $targetDir) -and -not $Force) {
|
|
43
|
+
Write-Host "REFUSING: destination exists: $targetDir (pass -Force to replace it)" -ForegroundColor Red
|
|
44
|
+
exit 2
|
|
45
|
+
}
|
|
46
|
+
if ((Test-Path -LiteralPath $targetDir) -and $Force) {
|
|
47
|
+
Remove-Item -LiteralPath $targetDir -Recurse -Force
|
|
48
|
+
}
|
|
49
|
+
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
|
|
50
|
+
Copy-Item -Path (Join-Path $sourceFull '*') -Destination $targetDir -Recurse -Force
|
|
51
|
+
$targetItem = Get-Item -LiteralPath $targetDir
|
|
52
|
+
} else {
|
|
53
|
+
$targetDir = $Destination
|
|
54
|
+
if (Test-Path -LiteralPath $targetDir -PathType Leaf) {
|
|
55
|
+
$targetDir = Split-Path -Parent $targetDir
|
|
56
|
+
}
|
|
57
|
+
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
|
|
58
|
+
$targetFile = Join-Path $targetDir $sourceItem.Name
|
|
59
|
+
if ((Test-Path -LiteralPath $targetFile) -and -not $Force) {
|
|
60
|
+
Write-Host "REFUSING: destination exists: $targetFile (pass -Force to replace it)" -ForegroundColor Red
|
|
61
|
+
exit 2
|
|
62
|
+
}
|
|
63
|
+
Copy-Item -LiteralPath $sourceFull -Destination $targetFile -Force
|
|
64
|
+
$targetItem = Get-Item -LiteralPath $targetFile
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Make the staged copy's label explicit instead of inherited. The destination is normally Medium
|
|
68
|
+
# already, but %LOCALAPPDATA% can itself be inside a tree that a sandbox has labelled.
|
|
69
|
+
$stagedPath = $targetItem.FullName
|
|
70
|
+
Write-Host "staged: $stagedPath"
|
|
71
|
+
if ($targetItem.PSIsContainer) {
|
|
72
|
+
& icacls $stagedPath /setintegritylevel '(OI)(CI)Medium' /T /C /Q | Out-Null
|
|
73
|
+
} else {
|
|
74
|
+
& icacls $stagedPath /setintegritylevel Medium | Out-Null
|
|
75
|
+
}
|
|
76
|
+
if ($LASTEXITCODE -ne 0) {
|
|
77
|
+
Write-Host "WARNING: could not set the staged copy's integrity label (icacls exit $LASTEXITCODE)" -ForegroundColor Yellow
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
$probe = Join-Path $PSScriptRoot 'integrity-probe.ps1'
|
|
81
|
+
if (Test-Path -LiteralPath $probe) {
|
|
82
|
+
$probeDir = if ($targetItem.PSIsContainer) { $stagedPath } else { $targetDir }
|
|
83
|
+
& $probe -Workspace $probeDir
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if ($Launch) {
|
|
87
|
+
if ($targetItem.PSIsContainer) {
|
|
88
|
+
$exe = $null
|
|
89
|
+
if (-not [string]::IsNullOrEmpty($ExeName)) {
|
|
90
|
+
$candidate = Join-Path $stagedPath $ExeName
|
|
91
|
+
if (Test-Path -LiteralPath $candidate) { $exe = $candidate }
|
|
92
|
+
} else {
|
|
93
|
+
$exe = @(Get-ChildItem -LiteralPath $stagedPath -Filter '*.exe' -File -ErrorAction SilentlyContinue | Select-Object -First 1)[0].FullName
|
|
94
|
+
}
|
|
95
|
+
if ([string]::IsNullOrEmpty($exe)) {
|
|
96
|
+
Write-Host 'REFUSING to launch: no .exe in the staged directory (pass -ExeName)' -ForegroundColor Red
|
|
97
|
+
exit 3
|
|
98
|
+
}
|
|
99
|
+
if ([string]::IsNullOrEmpty($Arguments)) {
|
|
100
|
+
Start-Process -FilePath $exe -WorkingDirectory $stagedPath
|
|
101
|
+
} else {
|
|
102
|
+
Start-Process -FilePath $exe -ArgumentList $Arguments -WorkingDirectory $stagedPath
|
|
103
|
+
}
|
|
104
|
+
Write-Host "launched: $exe"
|
|
105
|
+
} else {
|
|
106
|
+
if ([string]::IsNullOrEmpty($Arguments)) {
|
|
107
|
+
Start-Process -FilePath $stagedPath -WorkingDirectory $targetDir
|
|
108
|
+
} else {
|
|
109
|
+
Start-Process -FilePath $stagedPath -ArgumentList $Arguments -WorkingDirectory $targetDir
|
|
110
|
+
}
|
|
111
|
+
Write-Host "launched: $stagedPath"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
Write-Host 'RESULT: OK -- the app is staged outside the workspace.' -ForegroundColor Green
|
|
116
|
+
exit 0
|