@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,157 @@
|
|
|
1
|
+
// workspace-integrity.mjs -- clear the stale Low mandatory label a full-access workspace inherits.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS IS NEEDED, measured on a real Windows worker (2026-10-19):
|
|
4
|
+
//
|
|
5
|
+
// DSH's `workspace-write` sandbox does not only lower the child token to Low integrity
|
|
6
|
+
// (S-1-16-4096). It also writes an inheritable Low mandatory label onto the workspace directory and
|
|
7
|
+
// its temp root. Anything copied into that tree inherits the label -- and Windows launches an image
|
|
8
|
+
// carrying a Low label at Low integrity even when the parent token is High. The result is a GUI
|
|
9
|
+
// application that cannot write `%LOCALAPPDATA%` (Medium) while the agent shell around it can:
|
|
10
|
+
//
|
|
11
|
+
// pwsh (agent shell) -> High
|
|
12
|
+
// Start-Process C:\Windows\System32\whoami.exe -> High
|
|
13
|
+
// copy of whoami.exe under the workspace -> Low
|
|
14
|
+
//
|
|
15
|
+
// The label outlives the session that added it, so switching the worker to `danger-full-access`
|
|
16
|
+
// does not undo it: the stale label keeps downgrading every app in the workspace. Under full access
|
|
17
|
+
// the confinement has been deliberately turned off, so the Low label is not protecting anything --
|
|
18
|
+
// it is a leftover, and it actively breaks the GUI workflow that full access exists to enable.
|
|
19
|
+
//
|
|
20
|
+
// THE REPAIR IS DELIBERATELY NARROW:
|
|
21
|
+
// * only on Windows;
|
|
22
|
+
// * only when the effective mode is `danger-full-access` (a workspace-write worker MUST keep its
|
|
23
|
+
// Low label -- that label is what confines it);
|
|
24
|
+
// * only for an explicit task directory (`--task-dir`), or when `--repair-integrity` asks for the
|
|
25
|
+
// relay's inherited cwd;
|
|
26
|
+
// * never for a drive root, the Windows/Program Files/ProgramData trees, or the user profile root.
|
|
27
|
+
//
|
|
28
|
+
// `icacls /T` is recursive because changing only the top directory leaves nested directories and
|
|
29
|
+
// the executables already inside them at Low (measured: a parent relabel updates a direct child but
|
|
30
|
+
// not a file two levels down). It is idempotent and only runs once per relay start.
|
|
31
|
+
|
|
32
|
+
import { spawnSync } from 'node:child_process'
|
|
33
|
+
import { existsSync, statSync } from 'node:fs'
|
|
34
|
+
import { join, parse, resolve } from 'node:path'
|
|
35
|
+
|
|
36
|
+
/** Every environment variable that names a directory this module must never relabel. */
|
|
37
|
+
const PROTECTED_ROOTS = ['SystemRoot', 'windir', 'ProgramFiles', 'ProgramFiles(x86)', 'ProgramW6432', 'ProgramData']
|
|
38
|
+
|
|
39
|
+
/** Case-insensitive comparison form for a Windows path, without a trailing separator. */
|
|
40
|
+
export function normalizePath(path) {
|
|
41
|
+
if (typeof path !== 'string' || path.trim() === '') return ''
|
|
42
|
+
return resolve(path).replace(/[\\/]+$/u, '').toLowerCase()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Whether `path` is a system location whose mandatory label must not be touched.
|
|
47
|
+
*
|
|
48
|
+
* A worker whose relay inherited `C:\Windows\System32` as its cwd (a scheduled task with no
|
|
49
|
+
* explicit working directory) would otherwise have its whole System32 tree relabeled on startup.
|
|
50
|
+
* That is far worse than the problem being fixed, so the guard errs toward refusing.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} path
|
|
53
|
+
* @param {Record<string, string | undefined>} [env]
|
|
54
|
+
* @returns {boolean}
|
|
55
|
+
*/
|
|
56
|
+
export function isProtectedWorkspace(path, env = process.env) {
|
|
57
|
+
const target = normalizePath(path)
|
|
58
|
+
if (target === '') return true
|
|
59
|
+
// A drive root ("c:\\") or a UNC share root is not a workspace.
|
|
60
|
+
if (target === normalizePath(parse(resolve(path)).root)) return true
|
|
61
|
+
for (const name of PROTECTED_ROOTS) {
|
|
62
|
+
const base = env[name]
|
|
63
|
+
if (typeof base !== 'string' || base.trim() === '') continue
|
|
64
|
+
const root = normalizePath(base)
|
|
65
|
+
if (target === root || target.startsWith(`${root}\\`)) return true
|
|
66
|
+
}
|
|
67
|
+
// The user profile root itself is not a workspace either; `C:\Users\<name>\work` is fine.
|
|
68
|
+
const profile = env.USERPROFILE
|
|
69
|
+
if (typeof profile === 'string' && profile.trim() !== '' && target === normalizePath(profile)) return true
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Relabel one workspace tree from Low to Medium, when full access makes that the correct state.
|
|
75
|
+
*
|
|
76
|
+
* @param {object} [deps]
|
|
77
|
+
* @param {string} [deps.mode] the EFFECTIVE session mode the relay will spawn
|
|
78
|
+
* @param {string} [deps.dir] the task directory
|
|
79
|
+
* @param {boolean} [deps.declared] true when `--task-dir` named it explicitly
|
|
80
|
+
* @param {boolean} [deps.force] `--repair-integrity`: allow the inherited cwd too
|
|
81
|
+
* @param {number} [deps.timeoutMs] bound on the recursive pass
|
|
82
|
+
* @param {(file: string, args: string[], opts: object) => object} [deps.spawn] injected for tests
|
|
83
|
+
* @returns {{ applied: boolean, skipped: boolean, reason: string, error: string,
|
|
84
|
+
* dir: string, durationMs: number, timedOut: boolean }}
|
|
85
|
+
*/
|
|
86
|
+
export function repairWorkspaceIntegrity(deps = {}) {
|
|
87
|
+
const started = Date.now()
|
|
88
|
+
const result = (fields) => ({
|
|
89
|
+
applied: false,
|
|
90
|
+
skipped: true,
|
|
91
|
+
reason: '',
|
|
92
|
+
error: '',
|
|
93
|
+
dir: '',
|
|
94
|
+
durationMs: Date.now() - started,
|
|
95
|
+
timedOut: false,
|
|
96
|
+
...fields,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
if (process.platform !== 'win32') return result({ reason: 'mandatory labels are a Windows concept' })
|
|
100
|
+
|
|
101
|
+
const mode = String(deps.mode ?? '')
|
|
102
|
+
if (mode !== 'danger-full-access') {
|
|
103
|
+
return result({ reason: `effective mode is ${mode === '' ? 'not set' : mode}; a confined workspace must keep its Low label` })
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const dir = typeof deps.dir === 'string' ? deps.dir.trim() : ''
|
|
107
|
+
if (dir === '') return result({ reason: 'no task directory to repair' })
|
|
108
|
+
if (deps.declared !== true && deps.force !== true) {
|
|
109
|
+
return result({ reason: 'the task directory was inherited, not named with --task-dir (pass --repair-integrity to allow it)' })
|
|
110
|
+
}
|
|
111
|
+
if (!existsSync(dir)) return result({ reason: `task directory does not exist: ${dir}` })
|
|
112
|
+
try {
|
|
113
|
+
if (!statSync(dir).isDirectory()) return result({ reason: `task directory is not a directory: ${dir}` })
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return result({ reason: `cannot inspect ${dir}: ${error instanceof Error ? error.message : String(error)}` })
|
|
116
|
+
}
|
|
117
|
+
if (isProtectedWorkspace(dir)) {
|
|
118
|
+
return result({ reason: `refusing to relabel a protected system location: ${dir}` })
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const icacls = join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'icacls.exe')
|
|
122
|
+
const executable = existsSync(icacls) ? icacls : 'icacls.exe'
|
|
123
|
+
const args = [dir, '/setintegritylevel', '(OI)(CI)Medium', '/T', '/C', '/Q']
|
|
124
|
+
const run = (deps.spawn ?? spawnSync)(executable, args, {
|
|
125
|
+
encoding: 'utf8',
|
|
126
|
+
timeout: deps.timeoutMs ?? 60_000,
|
|
127
|
+
windowsHide: true,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const timedOut = run?.error?.code === 'ETIMEDOUT'
|
|
131
|
+
if (run?.error !== undefined && run.error !== null && !timedOut) {
|
|
132
|
+
return result({
|
|
133
|
+
dir,
|
|
134
|
+
reason: 'icacls could not run',
|
|
135
|
+
error: run.error instanceof Error ? run.error.message : String(run.error),
|
|
136
|
+
timedOut,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
if (!timedOut && run?.status !== 0) {
|
|
140
|
+
return result({
|
|
141
|
+
dir,
|
|
142
|
+
reason: 'icacls refused',
|
|
143
|
+
error: String(run?.stderr ?? '').trim().slice(0, 400) || `exit ${String(run?.status)}`,
|
|
144
|
+
timedOut,
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return result({
|
|
149
|
+
applied: true,
|
|
150
|
+
skipped: false,
|
|
151
|
+
dir,
|
|
152
|
+
timedOut,
|
|
153
|
+
reason: timedOut
|
|
154
|
+
? 'the recursive relabel hit its time limit; part of the tree may still be Low'
|
|
155
|
+
: 'workspace relabeled to Medium because this session runs at danger-full-access',
|
|
156
|
+
})
|
|
157
|
+
}
|