@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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +207 -2
  3. package/README.zh.md +195 -0
  4. package/assets/icon.svg +47 -0
  5. package/cordis.patch.yml +41 -0
  6. package/lib/bridge/acp-bridge.js +905 -0
  7. package/lib/bridge/peer-connection.js +730 -0
  8. package/lib/client.js +2842 -0
  9. package/lib/core/acp-model-overlay.js +121 -0
  10. package/lib/core/approval-queue.js +216 -0
  11. package/lib/core/audit-log.js +322 -0
  12. package/lib/core/desktop-notify.js +173 -0
  13. package/lib/core/environment-prompt.js +76 -0
  14. package/lib/core/relay-supervisor.js +278 -0
  15. package/lib/core/review-engine.js +679 -0
  16. package/lib/core/sandbox-diagnosis.js +258 -0
  17. package/lib/core/sandbox-probe.js +310 -0
  18. package/lib/core/self-acp-spec.js +117 -0
  19. package/lib/core/selfcheck-runner.js +150 -0
  20. package/lib/core/settings-store.js +226 -0
  21. package/lib/core/shot-gc.js +334 -0
  22. package/lib/core/task-cwd.js +50 -0
  23. package/lib/core/task-memory.js +163 -0
  24. package/lib/core/task-registry.js +238 -0
  25. package/lib/core/toolcall-cache.js +206 -0
  26. package/lib/core/transfer-queue.js +58 -0
  27. package/lib/core/transfer.js +1144 -0
  28. package/lib/core/worker-arming.js +334 -0
  29. package/lib/index.js +2207 -0
  30. package/lib/provider/out-of-process-helpers.js +76 -0
  31. package/lib/provider/remote-provider.js +261 -0
  32. package/lib/tools/mediator-shim.mjs +180 -0
  33. package/lib/tools/prepare-sandbox-mediator.mjs +529 -0
  34. package/lib/tools/remote-tools.js +680 -0
  35. package/lib/tools/selfcheck-fix-prompt.md +206 -0
  36. package/locale/en.json +6 -0
  37. package/locale/zh.json +6 -0
  38. package/package.json +72 -4
  39. package/slave/acp-profile-approval.patch.yml +66 -0
  40. package/slave/file-transfer.mjs +552 -0
  41. package/slave/integrity-probe.ps1 +99 -0
  42. package/slave/make-launch-spec.mjs +184 -0
  43. package/slave/net-address.mjs +138 -0
  44. package/slave/node_modules/.package-lock.json +26 -0
  45. package/slave/node_modules/ws/LICENSE +20 -0
  46. package/slave/node_modules/ws/README.md +548 -0
  47. package/slave/node_modules/ws/browser.js +8 -0
  48. package/slave/node_modules/ws/index.js +22 -0
  49. package/slave/node_modules/ws/lib/buffer-util.js +131 -0
  50. package/slave/node_modules/ws/lib/constants.js +19 -0
  51. package/slave/node_modules/ws/lib/event-target.js +292 -0
  52. package/slave/node_modules/ws/lib/extension.js +203 -0
  53. package/slave/node_modules/ws/lib/limiter.js +55 -0
  54. package/slave/node_modules/ws/lib/permessage-deflate.js +530 -0
  55. package/slave/node_modules/ws/lib/receiver.js +743 -0
  56. package/slave/node_modules/ws/lib/sender.js +607 -0
  57. package/slave/node_modules/ws/lib/stream.js +161 -0
  58. package/slave/node_modules/ws/lib/subprotocol.js +62 -0
  59. package/slave/node_modules/ws/lib/validation.js +152 -0
  60. package/slave/node_modules/ws/lib/websocket-server.js +562 -0
  61. package/slave/node_modules/ws/lib/websocket.js +1421 -0
  62. package/slave/node_modules/ws/package.json +74 -0
  63. package/slave/node_modules/ws/wrapper.mjs +21 -0
  64. package/slave/pairing.mjs +175 -0
  65. package/slave/permission-pairing.mjs +240 -0
  66. package/slave/prune-shots.mjs +170 -0
  67. package/slave/relay.mjs +1351 -0
  68. package/slave/repair-workspace-integrity.ps1 +75 -0
  69. package/slave/screenshot.ps1 +114 -0
  70. package/slave/selfcheck.ps1 +350 -0
  71. package/slave/stage-app.ps1 +116 -0
  72. 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