@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,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screenshot housekeeping for the relay.
|
|
3
|
+
*
|
|
4
|
+
* Kept in its own module rather than inline because the relay is a long-lived script with no
|
|
5
|
+
* test harness of its own: extracting this is what makes the deletion rules testable. It has
|
|
6
|
+
* NO dependencies beyond `node:fs` and `node:path` so it can be copied to a worker alongside
|
|
7
|
+
* `relay.mjs` without dragging anything else along.
|
|
8
|
+
*
|
|
9
|
+
* THREE RULES, ALL LOAD-BEARING:
|
|
10
|
+
*
|
|
11
|
+
* 1. Only delete files matching the caller's pattern. The directory is normally ours, but
|
|
12
|
+
* `--shot-dir` is configurable, and a mistyped path must not turn into deleted user data.
|
|
13
|
+
*
|
|
14
|
+
* 2. Never throw. This runs inside the capture path; failing to delete an old file must not
|
|
15
|
+
* fail the capture that just succeeded.
|
|
16
|
+
*
|
|
17
|
+
* 3. A byte ceiling that is missing, zero or nonsense means "no byte ceiling" -- never "delete
|
|
18
|
+
* everything". See the note on `opts.maxBytes` below: zero is the value an unconfigured
|
|
19
|
+
* install has, so reading it literally would wipe the cache on the first capture after an
|
|
20
|
+
* upgrade.
|
|
21
|
+
*/
|
|
22
|
+
import { readdirSync, statSync, unlinkSync } from 'node:fs'
|
|
23
|
+
import { join } from 'node:path'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} dir
|
|
27
|
+
* @param {number} keep newest N to retain; non-finite or negative disables the count pass
|
|
28
|
+
* @param {RegExp} pattern which filenames this caller is allowed to delete
|
|
29
|
+
* @param {{ maxBytes?: number }} [opts] byte ceiling for the matching files
|
|
30
|
+
* @returns {{ removed: number, kept: number, freedBytes: number, overBudgetBytes: number }}
|
|
31
|
+
*/
|
|
32
|
+
export function pruneShots(dir, keep, pattern, opts = {}) {
|
|
33
|
+
// The result shape is CONSTANT across every call, including the three-argument one. An earlier
|
|
34
|
+
// revision gated `overBudgetBytes` on `arguments.length` to keep a strict shape assertion in the
|
|
35
|
+
// tests passing; that was the wrong trade. A return type that depends on how many arguments the
|
|
36
|
+
// caller passed is a trap for the next caller -- `opts` has a default, so the fourth argument
|
|
37
|
+
// already looks optional -- and the assertion it protected was testing the shape rather than the
|
|
38
|
+
// behaviour. The test now checks the fields it cares about and this always reports all four.
|
|
39
|
+
const nothing = { removed: 0, kept: 0, freedBytes: 0, overBudgetBytes: 0 }
|
|
40
|
+
if (typeof dir !== 'string' || dir === '') return nothing
|
|
41
|
+
// A non-finite or negative budget means "do not prune", never "delete everything". The
|
|
42
|
+
// dangerous reading of a missing setting is 0.
|
|
43
|
+
if (!Number.isFinite(keep) || keep < 0) return nothing
|
|
44
|
+
if (!(pattern instanceof RegExp)) return nothing
|
|
45
|
+
|
|
46
|
+
// The byte ceiling follows the same rule as the count budget, and for the same reason:
|
|
47
|
+
// `--keep-shot-bytes` is absent on every install that has not opted in, so it arrives as 0 --
|
|
48
|
+
// and a ceiling of 0 read literally means "keep nothing", i.e. delete the whole directory on
|
|
49
|
+
// the first capture after an upgrade. So <= 0, non-finite and non-numeric all mean "no byte
|
|
50
|
+
// budget", and the raw value is tested rather than coerced: `'64'` is not a number the caller
|
|
51
|
+
// clearly meant, and the count budget already refuses `'20'` for exactly this reason.
|
|
52
|
+
const maxBytes = Number.isFinite(opts?.maxBytes) && opts.maxBytes > 0 ? opts.maxBytes : 0
|
|
53
|
+
|
|
54
|
+
let names
|
|
55
|
+
try {
|
|
56
|
+
names = readdirSync(dir)
|
|
57
|
+
} catch {
|
|
58
|
+
// No directory yet is the normal first-run state.
|
|
59
|
+
return nothing
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const files = []
|
|
63
|
+
for (const name of names) {
|
|
64
|
+
if (!pattern.test(name)) continue
|
|
65
|
+
try {
|
|
66
|
+
const stat = statSync(join(dir, name))
|
|
67
|
+
if (stat.isFile()) files.push({ name, mtimeMs: stat.mtimeMs, size: stat.size })
|
|
68
|
+
} catch { /* vanished between listing and stat */ }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Newest first. The name breaks ties: these are captured in sequence, and a filesystem with
|
|
72
|
+
// coarse timestamps can hand out identical mtimes.
|
|
73
|
+
files.sort((a, b) => (b.mtimeMs - a.mtimeMs) || b.name.localeCompare(a.name))
|
|
74
|
+
|
|
75
|
+
// Pass 1: the count budget, which keeps the head of the sorted list. A file whose unlink failed
|
|
76
|
+
// is still on disk, so it stays a candidate for pass 2 and is still counted as "kept".
|
|
77
|
+
const keepCount = Math.max(0, Math.floor(keep))
|
|
78
|
+
const surviving = []
|
|
79
|
+
let removed = 0
|
|
80
|
+
let freedBytes = 0
|
|
81
|
+
for (const [index, file] of files.entries()) {
|
|
82
|
+
if (index < keepCount) { surviving.push(file); continue }
|
|
83
|
+
try {
|
|
84
|
+
unlinkSync(join(dir, file.name))
|
|
85
|
+
removed += 1
|
|
86
|
+
freedBytes += file.size
|
|
87
|
+
} catch { surviving.push(file) } // locked or already gone; the next capture tries again
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Pass 2: the byte ceiling, oldest first, so it only ever eats into what pass 1 was willing to
|
|
91
|
+
// keep. The loop stops at one file: the newest capture is the one the master is about to read,
|
|
92
|
+
// and deleting it would break the very download this housekeeping runs next to. A single file
|
|
93
|
+
// over the ceiling therefore survives and the overage is reported instead -- a directory that
|
|
94
|
+
// is one capture too large beats a screenshot that cannot be fetched.
|
|
95
|
+
let overBudgetBytes = 0
|
|
96
|
+
if (maxBytes > 0) {
|
|
97
|
+
let total = surviving.reduce((sum, file) => sum + file.size, 0)
|
|
98
|
+
for (let last = surviving.length; total > maxBytes && last > 1; last -= 1) {
|
|
99
|
+
const victim = surviving[last - 1]
|
|
100
|
+
try {
|
|
101
|
+
unlinkSync(join(dir, victim.name))
|
|
102
|
+
removed += 1
|
|
103
|
+
freedBytes += victim.size
|
|
104
|
+
total -= victim.size
|
|
105
|
+
} catch { /* locked: it stays, and its bytes still count against the ceiling */ }
|
|
106
|
+
}
|
|
107
|
+
overBudgetBytes = Math.max(0, total - maxBytes)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const kept = files.length - removed
|
|
111
|
+
return { removed, kept, freedBytes, overBudgetBytes }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* How much the matching files in `dir` occupy right now.
|
|
116
|
+
*
|
|
117
|
+
* WHY THIS IS SEPARATE FROM `pruneShots`: this is the reporting half of the pair, and the master
|
|
118
|
+
* asks for it on a liveness ping -- so it must be cheap. It never reads file CONTENTS (one
|
|
119
|
+
* `readdirSync` plus one `statSync` per matching name), which is what keeps a directory scan
|
|
120
|
+
* acceptable next to a heartbeat.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} dir
|
|
123
|
+
* @param {RegExp} pattern
|
|
124
|
+
* @returns {{ files: number, bytes: number, newestAt: number, oldestAt: number }}
|
|
125
|
+
*/
|
|
126
|
+
export function shotUsage(dir, pattern) {
|
|
127
|
+
const empty = { files: 0, bytes: 0, newestAt: 0, oldestAt: 0 }
|
|
128
|
+
if (typeof dir !== 'string' || dir === '') return empty
|
|
129
|
+
if (!(pattern instanceof RegExp)) return empty
|
|
130
|
+
|
|
131
|
+
let names
|
|
132
|
+
try {
|
|
133
|
+
names = readdirSync(dir)
|
|
134
|
+
} catch {
|
|
135
|
+
// A directory that is missing, or that this process may not read, has nothing to report.
|
|
136
|
+
// Neither is an error: a first run has no directory yet.
|
|
137
|
+
return empty
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let files = 0
|
|
141
|
+
let bytes = 0
|
|
142
|
+
let newestAt = 0
|
|
143
|
+
let oldestAt = 0
|
|
144
|
+
for (const name of names) {
|
|
145
|
+
if (!pattern.test(name)) continue
|
|
146
|
+
try {
|
|
147
|
+
const stat = statSync(join(dir, name))
|
|
148
|
+
// A DIRECTORY named like a capture is skipped rather than counted: nothing wrote it here,
|
|
149
|
+
// and `pruneShots` will not delete it either, so counting it would report bytes the cache
|
|
150
|
+
// can never reclaim.
|
|
151
|
+
if (!stat.isFile()) continue
|
|
152
|
+
if (files === 0) {
|
|
153
|
+
newestAt = stat.mtimeMs
|
|
154
|
+
oldestAt = stat.mtimeMs
|
|
155
|
+
} else {
|
|
156
|
+
if (stat.mtimeMs > newestAt) newestAt = stat.mtimeMs
|
|
157
|
+
if (stat.mtimeMs < oldestAt) oldestAt = stat.mtimeMs
|
|
158
|
+
}
|
|
159
|
+
files += 1
|
|
160
|
+
bytes += stat.size
|
|
161
|
+
} catch { /* vanished between listing and stat */ }
|
|
162
|
+
}
|
|
163
|
+
return { files, bytes, newestAt, oldestAt }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The names this relay writes: `shot-<epoch ms>-<sequence>.png`. */
|
|
167
|
+
export const RELAY_SHOT_PATTERN = /^shot-\d+-\d+\.png$/
|
|
168
|
+
|
|
169
|
+
/** Retry prunes to keep while the relay is still starting up. */
|
|
170
|
+
export const DEFAULT_KEEP_SHOTS = 20
|