dsh-remote 0.8.72 → 0.8.73
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 -21
- package/README.en.md +389 -389
- package/README.md +462 -462
- package/cordis.patch.yml +9 -9
- package/lib/binding.js +172 -172
- package/lib/client.js +4022 -4022
- package/lib/credential.js +263 -263
- package/lib/errors.js +141 -141
- package/lib/exec-job.js +235 -235
- package/lib/file-reference.js +457 -457
- package/lib/forwards.js +258 -258
- package/lib/hostkey.js +108 -108
- package/lib/http-transport.js +196 -196
- package/lib/ignore.js +202 -202
- package/lib/index.js +4550 -4540
- package/lib/instructions-dynamic.js +415 -415
- package/lib/openssh.js +879 -879
- package/lib/paths.js +252 -252
- package/lib/pool.js +963 -893
- package/lib/registry.js +296 -293
- package/lib/remote-fs.js +173 -173
- package/lib/remote-sandbox.js +183 -183
- package/lib/remote-shell-child.cjs +344 -305
- package/lib/remote-shell.js +542 -518
- package/lib/routes-fs.js +245 -245
- package/lib/routes-term.js +169 -169
- package/lib/search.js +235 -235
- package/lib/sftp-surface.js +119 -119
- package/lib/sshconfig.js +779 -705
- package/lib/sync.js +275 -275
- package/lib/tasks.js +95 -95
- package/lib/telemetry.js +213 -213
- package/lib/terminal.js +205 -205
- package/lib/update.js +472 -472
- package/lib/web-attach.js +850 -850
- package/package.json +100 -99
package/lib/remote-sandbox.js
CHANGED
|
@@ -1,183 +1,183 @@
|
|
|
1
|
-
// dsh-remote — remote sandbox semantics (issue: parity with local sandbox).
|
|
2
|
-
//
|
|
3
|
-
// The gap this closes: a local agent session runs under `ctx.sandboxPolicy`
|
|
4
|
-
// (read-only / workspace-write / danger-full-access), but every rw_* tool ran
|
|
5
|
-
// remote commands with NO policy at all. A confined session could therefore
|
|
6
|
-
// reach a remote host and mutate anything — the sandbox was not bypassed by a
|
|
7
|
-
// flaw in its enforcement, it simply had no say over a different machine.
|
|
8
|
-
//
|
|
9
|
-
// Why this is a TOOL-LAYER fence rather than `ctx.sandbox.confine()`: the core
|
|
10
|
-
// sandbox service wraps argv for a LOCAL spawn (`confine(argv, policy)` returns
|
|
11
|
-
// argv to spawn on THIS host). A remote host has no sandbox runner, so wrapping
|
|
12
|
-
// local argv cannot constrain a process on the other end of an SSH channel. The
|
|
13
|
-
// core's own docs say exactly this — "Containers, microVMs, and remote
|
|
14
|
-
// execution replace the surrounding capability seam instead". So the honest
|
|
15
|
-
// enforcement point is here: decide, per operation, whether this session's mode
|
|
16
|
-
// permits that remote path/command, and refuse with the OFFICIAL denial shape
|
|
17
|
-
// so the model's escalation affordances keep working.
|
|
18
|
-
//
|
|
19
|
-
// Threat model (stated plainly, matching the official dsh-fs-sandbox header):
|
|
20
|
-
// this is CONTAINMENT, not a security boundary. Paths are compared lexically, so
|
|
21
|
-
// symlinks and TOCTOU races inside the remote host can escape; a remote command
|
|
22
|
-
// can `cd` anywhere once bash runs with the user's own privileges. What it does
|
|
23
|
-
// buy, and what the issue asked for: a confined session can no longer mutate a
|
|
24
|
-
// remote tree outside its workspace BY ACCIDENT through the file tools, and
|
|
25
|
-
// `rw_exec` refuses outright under read-only instead of silently running
|
|
26
|
-
// anything. Deployments needing a hard boundary must confine on the REMOTE side
|
|
27
|
-
// (containers, per-user accounts, filesystem ACLs).
|
|
28
|
-
|
|
29
|
-
import path from 'node:path'
|
|
30
|
-
import { normalizeRemotePath, relPathUnder } from './paths.js'
|
|
31
|
-
|
|
32
|
-
/** The three file-effect modes, mirroring `@deepseek-ai/dsh-sandbox`. */
|
|
33
|
-
export const REMOTE_SANDBOX_MODES = ['read-only', 'workspace-write', 'danger-full-access']
|
|
34
|
-
|
|
35
|
-
/** Denial marker prefix, matching the official `sandboxDenialMarker` shape. */
|
|
36
|
-
const DENIAL_PREFIX = '[sandbox: '
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Whether `target` lies inside `root` using lexical, separator-aware
|
|
40
|
-
* comparison. Mirrors the official `isPathInsideWorkspace`: both sides are
|
|
41
|
-
* normalized so `..` cannot escape, and `/tmp/foo` does not contain
|
|
42
|
-
* `/tmp/foobar`.
|
|
43
|
-
*
|
|
44
|
-
* A root of `/` contains everything (the whole filesystem is the workspace),
|
|
45
|
-
* which is what makes "workspace = filesystem root" a legal, permissive setup
|
|
46
|
-
* (issue #60) rather than a denial of everything.
|
|
47
|
-
*
|
|
48
|
-
* @param {string} target - the remote path being touched.
|
|
49
|
-
* @param {string} root - the remote workspace root.
|
|
50
|
-
* @returns {boolean}
|
|
51
|
-
*/
|
|
52
|
-
export function isInsideRemote(target, root) {
|
|
53
|
-
if (typeof target !== 'string' || typeof root !== 'string') return false
|
|
54
|
-
// POSIX-absolute only. `normalizeRemotePath` deliberately coerces a relative
|
|
55
|
-
// input to an absolute one (`tmp/foo` → `/tmp/foo`, `../foo` → `/foo`), which
|
|
56
|
-
// is right for a user-supplied path but WRONG for a fence: it would turn an
|
|
57
|
-
// unprovable root into a confident one. A root that is not absolute proves
|
|
58
|
-
// nothing, so refuse before normalizing.
|
|
59
|
-
if (!isPosixAbsolute(root)) return false
|
|
60
|
-
const t = normalizeRemotePath(target)
|
|
61
|
-
let r = normalizeRemotePath(root)
|
|
62
|
-
if (r === '/' || r === '') return true
|
|
63
|
-
r = r.replace(/\/+$/, '')
|
|
64
|
-
if (t === r) return true
|
|
65
|
-
return t.startsWith(r + '/')
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Whether a path is an absolute POSIX or Windows-Drive path, WITHOUT the
|
|
70
|
-
* coercing normalization `normalizeRemotePath` performs.
|
|
71
|
-
*
|
|
72
|
-
* @param {string} p
|
|
73
|
-
* @returns {boolean}
|
|
74
|
-
*/
|
|
75
|
-
export function isPosixAbsolute(p) {
|
|
76
|
-
const s = String(p || '')
|
|
77
|
-
if (!s) return false
|
|
78
|
-
if (s.startsWith('/')) return true // POSIX
|
|
79
|
-
if (/^[a-zA-Z]:[\\/]/.test(s)) return true // Windows drive
|
|
80
|
-
if (/^\\\\[^\\]+/.test(s)) return true // Windows UNC
|
|
81
|
-
return false
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Decide whether one remote operation is permitted under a mode.
|
|
86
|
-
*
|
|
87
|
-
* `operation` is the coarse kind the tools act with:
|
|
88
|
-
* - `read` — reading remote data (never denied: read-only still reads)
|
|
89
|
-
* - `write` — creating/replacing/removing a REMOTE PATH (path fence)
|
|
90
|
-
* - `exec` — running an arbitrary remote command
|
|
91
|
-
* - `tunnel` — opening a port forward
|
|
92
|
-
* - `connect`— dialing/authenticating a machine (never denied: without it
|
|
93
|
-
* nothing else could work, and it changes no remote state)
|
|
94
|
-
*
|
|
95
|
-
* @param {object} arg
|
|
96
|
-
* @param {string} arg.mode - the resolved sandbox mode.
|
|
97
|
-
* @param {string} arg.operation - see above.
|
|
98
|
-
* @param {string} [arg.path] - the remote path a `write` targets.
|
|
99
|
-
* @param {string} arg.workspaceRoot - the remote workspace root.
|
|
100
|
-
* @returns {{allowed: boolean, reason?: string, mode: string, subject?: string}}
|
|
101
|
-
*/
|
|
102
|
-
export function checkRemoteSandbox({ mode, operation, path: target, workspaceRoot }) {
|
|
103
|
-
const m = REMOTE_SANDBOX_MODES.includes(mode) ? mode : 'read-only'
|
|
104
|
-
// Fail closed on an unknown mode, exactly like the official renderPolicyContext.
|
|
105
|
-
const effective = m
|
|
106
|
-
|
|
107
|
-
if (operation === 'read' || operation === 'connect') return { allowed: true, mode: effective }
|
|
108
|
-
|
|
109
|
-
if (effective === 'danger-full-access') return { allowed: true, mode: effective }
|
|
110
|
-
|
|
111
|
-
if (effective === 'read-only') {
|
|
112
|
-
// read-only refuses every state-changing remote operation. This is the
|
|
113
|
-
// strict reading of the official mode ("`read-only` permits only required
|
|
114
|
-
// sinks such as /dev/null") and the one that keeps a confined session
|
|
115
|
-
// honest on a machine whose filesystem it cannot police.
|
|
116
|
-
return {
|
|
117
|
-
allowed: false,
|
|
118
|
-
mode: effective,
|
|
119
|
-
subject: operation === 'exec' ? 'command' : operation === 'tunnel' ? 'tunnel' : 'operation',
|
|
120
|
-
reason:
|
|
121
|
-
operation === 'exec'
|
|
122
|
-
? 'read-only sessions may not run commands on a remote host'
|
|
123
|
-
: operation === 'tunnel'
|
|
124
|
-
? 'read-only sessions may not open remote port forwards'
|
|
125
|
-
: 'read-only sessions may not modify a remote host',
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// workspace-write: writes must stay inside the workspace; exec and tunnel are
|
|
130
|
-
// allowed because a command's effects cannot be bounded lexically (and the
|
|
131
|
-
// official bash tool under workspace-write DOES run commands, confining them
|
|
132
|
-
// via the OS). Refusing exec here would make workspace-write useless for the
|
|
133
|
-
// primary remote use case (builds, tests), while pretending to confine it
|
|
134
|
-
// would be worse than saying we do not.
|
|
135
|
-
if (operation === 'exec' || operation === 'tunnel') return { allowed: true, mode: effective }
|
|
136
|
-
|
|
137
|
-
// A write with no resolvable workspace root cannot be proven inside it.
|
|
138
|
-
if (!workspaceRoot) {
|
|
139
|
-
return {
|
|
140
|
-
allowed: false,
|
|
141
|
-
mode: effective,
|
|
142
|
-
subject: 'operation',
|
|
143
|
-
reason: 'workspace-write needs a remote workspace root to fence writes; none is set',
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
if (isInsideRemote(target, workspaceRoot)) return { allowed: true, mode: effective }
|
|
147
|
-
return {
|
|
148
|
-
allowed: false,
|
|
149
|
-
mode: effective,
|
|
150
|
-
subject: 'operation',
|
|
151
|
-
reason: `workspace-write only permits changes inside ${workspaceRoot}`,
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Render a denial into the official error shape so the model sees the same
|
|
157
|
-
* thing it would from a local sandbox denial (and can use the same escalation
|
|
158
|
-
* affordances when the composition advertises them).
|
|
159
|
-
*
|
|
160
|
-
* @param {{mode: string, reason?: string, subject?: string}} denial
|
|
161
|
-
* @returns {Error}
|
|
162
|
-
*/
|
|
163
|
-
export function remoteSandboxDenial(denial) {
|
|
164
|
-
const mode = (denial && denial.mode) || 'read-only'
|
|
165
|
-
const subject = (denial && denial.subject) || 'operation'
|
|
166
|
-
const hint = `[escalation: raise sandbox mode to retry the ${subject} once]`
|
|
167
|
-
const err = new Error(`${DENIAL_PREFIX}${mode}]\n${denial && denial.reason ? denial.reason + '\n' : ''}${hint}`)
|
|
168
|
-
err.code = 'FS_SANDBOX_DENIED'
|
|
169
|
-
err.remoteSandbox = { mode, subject }
|
|
170
|
-
return err
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Relative path of a remote target under the workspace root, for diagnostics.
|
|
175
|
-
* Returns null when the target is outside it.
|
|
176
|
-
* @param {string} target
|
|
177
|
-
* @param {string} root
|
|
178
|
-
* @returns {string|null}
|
|
179
|
-
*/
|
|
180
|
-
export function remoteRelUnder(target, root) {
|
|
181
|
-
const rel = relPathUnder(root, target)
|
|
182
|
-
return rel === null ? null : rel
|
|
183
|
-
}
|
|
1
|
+
// dsh-remote — remote sandbox semantics (issue: parity with local sandbox).
|
|
2
|
+
//
|
|
3
|
+
// The gap this closes: a local agent session runs under `ctx.sandboxPolicy`
|
|
4
|
+
// (read-only / workspace-write / danger-full-access), but every rw_* tool ran
|
|
5
|
+
// remote commands with NO policy at all. A confined session could therefore
|
|
6
|
+
// reach a remote host and mutate anything — the sandbox was not bypassed by a
|
|
7
|
+
// flaw in its enforcement, it simply had no say over a different machine.
|
|
8
|
+
//
|
|
9
|
+
// Why this is a TOOL-LAYER fence rather than `ctx.sandbox.confine()`: the core
|
|
10
|
+
// sandbox service wraps argv for a LOCAL spawn (`confine(argv, policy)` returns
|
|
11
|
+
// argv to spawn on THIS host). A remote host has no sandbox runner, so wrapping
|
|
12
|
+
// local argv cannot constrain a process on the other end of an SSH channel. The
|
|
13
|
+
// core's own docs say exactly this — "Containers, microVMs, and remote
|
|
14
|
+
// execution replace the surrounding capability seam instead". So the honest
|
|
15
|
+
// enforcement point is here: decide, per operation, whether this session's mode
|
|
16
|
+
// permits that remote path/command, and refuse with the OFFICIAL denial shape
|
|
17
|
+
// so the model's escalation affordances keep working.
|
|
18
|
+
//
|
|
19
|
+
// Threat model (stated plainly, matching the official dsh-fs-sandbox header):
|
|
20
|
+
// this is CONTAINMENT, not a security boundary. Paths are compared lexically, so
|
|
21
|
+
// symlinks and TOCTOU races inside the remote host can escape; a remote command
|
|
22
|
+
// can `cd` anywhere once bash runs with the user's own privileges. What it does
|
|
23
|
+
// buy, and what the issue asked for: a confined session can no longer mutate a
|
|
24
|
+
// remote tree outside its workspace BY ACCIDENT through the file tools, and
|
|
25
|
+
// `rw_exec` refuses outright under read-only instead of silently running
|
|
26
|
+
// anything. Deployments needing a hard boundary must confine on the REMOTE side
|
|
27
|
+
// (containers, per-user accounts, filesystem ACLs).
|
|
28
|
+
|
|
29
|
+
import path from 'node:path'
|
|
30
|
+
import { normalizeRemotePath, relPathUnder } from './paths.js'
|
|
31
|
+
|
|
32
|
+
/** The three file-effect modes, mirroring `@deepseek-ai/dsh-sandbox`. */
|
|
33
|
+
export const REMOTE_SANDBOX_MODES = ['read-only', 'workspace-write', 'danger-full-access']
|
|
34
|
+
|
|
35
|
+
/** Denial marker prefix, matching the official `sandboxDenialMarker` shape. */
|
|
36
|
+
const DENIAL_PREFIX = '[sandbox: '
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Whether `target` lies inside `root` using lexical, separator-aware
|
|
40
|
+
* comparison. Mirrors the official `isPathInsideWorkspace`: both sides are
|
|
41
|
+
* normalized so `..` cannot escape, and `/tmp/foo` does not contain
|
|
42
|
+
* `/tmp/foobar`.
|
|
43
|
+
*
|
|
44
|
+
* A root of `/` contains everything (the whole filesystem is the workspace),
|
|
45
|
+
* which is what makes "workspace = filesystem root" a legal, permissive setup
|
|
46
|
+
* (issue #60) rather than a denial of everything.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} target - the remote path being touched.
|
|
49
|
+
* @param {string} root - the remote workspace root.
|
|
50
|
+
* @returns {boolean}
|
|
51
|
+
*/
|
|
52
|
+
export function isInsideRemote(target, root) {
|
|
53
|
+
if (typeof target !== 'string' || typeof root !== 'string') return false
|
|
54
|
+
// POSIX-absolute only. `normalizeRemotePath` deliberately coerces a relative
|
|
55
|
+
// input to an absolute one (`tmp/foo` → `/tmp/foo`, `../foo` → `/foo`), which
|
|
56
|
+
// is right for a user-supplied path but WRONG for a fence: it would turn an
|
|
57
|
+
// unprovable root into a confident one. A root that is not absolute proves
|
|
58
|
+
// nothing, so refuse before normalizing.
|
|
59
|
+
if (!isPosixAbsolute(root)) return false
|
|
60
|
+
const t = normalizeRemotePath(target)
|
|
61
|
+
let r = normalizeRemotePath(root)
|
|
62
|
+
if (r === '/' || r === '') return true
|
|
63
|
+
r = r.replace(/\/+$/, '')
|
|
64
|
+
if (t === r) return true
|
|
65
|
+
return t.startsWith(r + '/')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Whether a path is an absolute POSIX or Windows-Drive path, WITHOUT the
|
|
70
|
+
* coercing normalization `normalizeRemotePath` performs.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} p
|
|
73
|
+
* @returns {boolean}
|
|
74
|
+
*/
|
|
75
|
+
export function isPosixAbsolute(p) {
|
|
76
|
+
const s = String(p || '')
|
|
77
|
+
if (!s) return false
|
|
78
|
+
if (s.startsWith('/')) return true // POSIX
|
|
79
|
+
if (/^[a-zA-Z]:[\\/]/.test(s)) return true // Windows drive
|
|
80
|
+
if (/^\\\\[^\\]+/.test(s)) return true // Windows UNC
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Decide whether one remote operation is permitted under a mode.
|
|
86
|
+
*
|
|
87
|
+
* `operation` is the coarse kind the tools act with:
|
|
88
|
+
* - `read` — reading remote data (never denied: read-only still reads)
|
|
89
|
+
* - `write` — creating/replacing/removing a REMOTE PATH (path fence)
|
|
90
|
+
* - `exec` — running an arbitrary remote command
|
|
91
|
+
* - `tunnel` — opening a port forward
|
|
92
|
+
* - `connect`— dialing/authenticating a machine (never denied: without it
|
|
93
|
+
* nothing else could work, and it changes no remote state)
|
|
94
|
+
*
|
|
95
|
+
* @param {object} arg
|
|
96
|
+
* @param {string} arg.mode - the resolved sandbox mode.
|
|
97
|
+
* @param {string} arg.operation - see above.
|
|
98
|
+
* @param {string} [arg.path] - the remote path a `write` targets.
|
|
99
|
+
* @param {string} arg.workspaceRoot - the remote workspace root.
|
|
100
|
+
* @returns {{allowed: boolean, reason?: string, mode: string, subject?: string}}
|
|
101
|
+
*/
|
|
102
|
+
export function checkRemoteSandbox({ mode, operation, path: target, workspaceRoot }) {
|
|
103
|
+
const m = REMOTE_SANDBOX_MODES.includes(mode) ? mode : 'read-only'
|
|
104
|
+
// Fail closed on an unknown mode, exactly like the official renderPolicyContext.
|
|
105
|
+
const effective = m
|
|
106
|
+
|
|
107
|
+
if (operation === 'read' || operation === 'connect') return { allowed: true, mode: effective }
|
|
108
|
+
|
|
109
|
+
if (effective === 'danger-full-access') return { allowed: true, mode: effective }
|
|
110
|
+
|
|
111
|
+
if (effective === 'read-only') {
|
|
112
|
+
// read-only refuses every state-changing remote operation. This is the
|
|
113
|
+
// strict reading of the official mode ("`read-only` permits only required
|
|
114
|
+
// sinks such as /dev/null") and the one that keeps a confined session
|
|
115
|
+
// honest on a machine whose filesystem it cannot police.
|
|
116
|
+
return {
|
|
117
|
+
allowed: false,
|
|
118
|
+
mode: effective,
|
|
119
|
+
subject: operation === 'exec' ? 'command' : operation === 'tunnel' ? 'tunnel' : 'operation',
|
|
120
|
+
reason:
|
|
121
|
+
operation === 'exec'
|
|
122
|
+
? 'read-only sessions may not run commands on a remote host'
|
|
123
|
+
: operation === 'tunnel'
|
|
124
|
+
? 'read-only sessions may not open remote port forwards'
|
|
125
|
+
: 'read-only sessions may not modify a remote host',
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// workspace-write: writes must stay inside the workspace; exec and tunnel are
|
|
130
|
+
// allowed because a command's effects cannot be bounded lexically (and the
|
|
131
|
+
// official bash tool under workspace-write DOES run commands, confining them
|
|
132
|
+
// via the OS). Refusing exec here would make workspace-write useless for the
|
|
133
|
+
// primary remote use case (builds, tests), while pretending to confine it
|
|
134
|
+
// would be worse than saying we do not.
|
|
135
|
+
if (operation === 'exec' || operation === 'tunnel') return { allowed: true, mode: effective }
|
|
136
|
+
|
|
137
|
+
// A write with no resolvable workspace root cannot be proven inside it.
|
|
138
|
+
if (!workspaceRoot) {
|
|
139
|
+
return {
|
|
140
|
+
allowed: false,
|
|
141
|
+
mode: effective,
|
|
142
|
+
subject: 'operation',
|
|
143
|
+
reason: 'workspace-write needs a remote workspace root to fence writes; none is set',
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (isInsideRemote(target, workspaceRoot)) return { allowed: true, mode: effective }
|
|
147
|
+
return {
|
|
148
|
+
allowed: false,
|
|
149
|
+
mode: effective,
|
|
150
|
+
subject: 'operation',
|
|
151
|
+
reason: `workspace-write only permits changes inside ${workspaceRoot}`,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Render a denial into the official error shape so the model sees the same
|
|
157
|
+
* thing it would from a local sandbox denial (and can use the same escalation
|
|
158
|
+
* affordances when the composition advertises them).
|
|
159
|
+
*
|
|
160
|
+
* @param {{mode: string, reason?: string, subject?: string}} denial
|
|
161
|
+
* @returns {Error}
|
|
162
|
+
*/
|
|
163
|
+
export function remoteSandboxDenial(denial) {
|
|
164
|
+
const mode = (denial && denial.mode) || 'read-only'
|
|
165
|
+
const subject = (denial && denial.subject) || 'operation'
|
|
166
|
+
const hint = `[escalation: raise sandbox mode to retry the ${subject} once]`
|
|
167
|
+
const err = new Error(`${DENIAL_PREFIX}${mode}]\n${denial && denial.reason ? denial.reason + '\n' : ''}${hint}`)
|
|
168
|
+
err.code = 'FS_SANDBOX_DENIED'
|
|
169
|
+
err.remoteSandbox = { mode, subject }
|
|
170
|
+
return err
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Relative path of a remote target under the workspace root, for diagnostics.
|
|
175
|
+
* Returns null when the target is outside it.
|
|
176
|
+
* @param {string} target
|
|
177
|
+
* @param {string} root
|
|
178
|
+
* @returns {string|null}
|
|
179
|
+
*/
|
|
180
|
+
export function remoteRelUnder(target, root) {
|
|
181
|
+
const rel = relPathUnder(root, target)
|
|
182
|
+
return rel === null ? null : rel
|
|
183
|
+
}
|