farming-code 2.2.22 → 2.2.27
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/backend/native-pty-host-client.js +99 -7
- package/backend/native-pty-host-path.js +20 -0
- package/backend/native-pty-host.js +46 -9
- package/backend/workspace-file-router.js +30 -6
- package/dist/assets/{App-CmZdNavW.js → App-Cm8qTjsc.js} +24 -24
- package/dist/assets/{FileEditorMarkdownPreview-KbnE8VC8.js → FileEditorMarkdownPreview-Da5jguUs.js} +1 -1
- package/dist/assets/{FileEditorPane-5_HUejjd.js → FileEditorPane-fFqHA4wW.js} +2 -2
- package/dist/assets/{ProjectFilesSection-C1RAJWOE.js → ProjectFilesSection-Cyy3Vcn8.js} +1 -1
- package/dist/assets/file-icons-BqiH0rDI.js +1 -0
- package/dist/assets/{index-RvUiMpGP.js → index-CtwKYTiC.js} +2 -2
- package/dist/assets/{workspace-view-state-BqB4HM7r.js → workspace-view-state-BiuEbmRr.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/dist/assets/file-icons-CHWt5nSy.js +0 -1
|
@@ -4,7 +4,10 @@ const crypto = require('crypto');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const net = require('net');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const {
|
|
7
|
+
const {
|
|
8
|
+
nativePtyHostPrivateSocketNamePattern,
|
|
9
|
+
nativePtyHostSocketPath,
|
|
10
|
+
} = require('./native-pty-host-path');
|
|
8
11
|
const {
|
|
9
12
|
nativePtyHostRuntimeIdentity,
|
|
10
13
|
nativePtyHostRuntimeIdentityMatches,
|
|
@@ -17,6 +20,7 @@ const {
|
|
|
17
20
|
const { isTemporaryProviderSessionId } = require('./provider-session-id');
|
|
18
21
|
const storageLayout = require('./storage-layout');
|
|
19
22
|
const { deserializeTerminalState } = require('./terminal-state-serialization');
|
|
23
|
+
const { probeUnixSocket } = require('./terminal-runtime-cleanup');
|
|
20
24
|
|
|
21
25
|
const DEFAULT_CONNECT_RETRIES = 300;
|
|
22
26
|
const DEFAULT_CONNECT_RETRY_MS = 50;
|
|
@@ -184,6 +188,7 @@ class NativePtyHostClient extends EventEmitter {
|
|
|
184
188
|
this.controllerIdentityReady = null;
|
|
185
189
|
this.hostRotationTimeoutMs = options.hostRotationTimeoutMs || DEFAULT_HOST_ROTATION_TIMEOUT_MS;
|
|
186
190
|
this.connectedHostInfo = null;
|
|
191
|
+
this.connectedSocketPath = '';
|
|
187
192
|
this.runtimeRotationInfo = null;
|
|
188
193
|
this.socket = null;
|
|
189
194
|
this.socketGeneration = 0;
|
|
@@ -235,7 +240,85 @@ class NativePtyHostClient extends EventEmitter {
|
|
|
235
240
|
|
|
236
241
|
canConnectWithoutStartingHost() {
|
|
237
242
|
if (process.platform === 'win32') return true;
|
|
238
|
-
return fs.existsSync(this.socketPath);
|
|
243
|
+
return fs.existsSync(this.socketPath) || this.privateSocketCandidates().length > 0;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
privateSocketCandidates() {
|
|
247
|
+
if (process.platform === 'win32') return [];
|
|
248
|
+
const directory = path.dirname(this.socketPath);
|
|
249
|
+
const pattern = nativePtyHostPrivateSocketNamePattern(this.socketPath);
|
|
250
|
+
try {
|
|
251
|
+
return fs.readdirSync(directory)
|
|
252
|
+
.filter(name => pattern.test(name))
|
|
253
|
+
.map(name => path.join(directory, name));
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (error?.code === 'ENOENT') return [];
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
connectedPrivateSocketPath(hostInfo = {}) {
|
|
261
|
+
if (process.platform === 'win32') return '';
|
|
262
|
+
if (typeof hostInfo.privateSocketPath === 'string' && hostInfo.privateSocketPath) {
|
|
263
|
+
return hostInfo.privateSocketPath;
|
|
264
|
+
}
|
|
265
|
+
if (!this.connectedSocketPath || this.connectedSocketPath !== this.socketPath) {
|
|
266
|
+
return this.connectedSocketPath;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let publicIdentity;
|
|
270
|
+
try {
|
|
271
|
+
const stat = fs.statSync(this.socketPath, { bigint: true });
|
|
272
|
+
publicIdentity = { dev: stat.dev, ino: stat.ino };
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (error?.code === 'ENOENT') return '';
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const matching = this.privateSocketCandidates().filter((candidate) => {
|
|
279
|
+
try {
|
|
280
|
+
const stat = fs.statSync(candidate, { bigint: true });
|
|
281
|
+
return stat.dev === publicIdentity.dev && stat.ino === publicIdentity.ino;
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (error?.code === 'ENOENT') return false;
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
return matching.length === 1 ? matching[0] : '';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async resolveConnectSocketPath() {
|
|
291
|
+
if (process.platform === 'win32' || fs.existsSync(this.socketPath)) {
|
|
292
|
+
return this.socketPath;
|
|
293
|
+
}
|
|
294
|
+
const active = [];
|
|
295
|
+
for (const candidate of this.privateSocketCandidates()) {
|
|
296
|
+
const probe = await probeUnixSocket(candidate);
|
|
297
|
+
if (probe.active) active.push(candidate);
|
|
298
|
+
}
|
|
299
|
+
if (active.length > 1) {
|
|
300
|
+
const error = new Error('Multiple live native PTY hosts were found for this Farming config');
|
|
301
|
+
error.code = 'FARMING_NATIVE_HOST_AMBIGUOUS';
|
|
302
|
+
error.socketPaths = active;
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
return active[0] || this.socketPath;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
restorePublicSocketPath(connectedPath) {
|
|
309
|
+
if (
|
|
310
|
+
process.platform === 'win32'
|
|
311
|
+
|| !connectedPath
|
|
312
|
+
|| connectedPath === this.socketPath
|
|
313
|
+
|| fs.existsSync(this.socketPath)
|
|
314
|
+
) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
fs.linkSync(connectedPath, this.socketPath);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
if (!error || error.code !== 'EEXIST') throw error;
|
|
321
|
+
}
|
|
239
322
|
}
|
|
240
323
|
|
|
241
324
|
spawnHost() {
|
|
@@ -288,9 +371,10 @@ class NativePtyHostClient extends EventEmitter {
|
|
|
288
371
|
});
|
|
289
372
|
}
|
|
290
373
|
|
|
291
|
-
connectOnce() {
|
|
374
|
+
async connectOnce() {
|
|
375
|
+
const connectedPath = await this.resolveConnectSocketPath();
|
|
292
376
|
return new Promise((resolve, reject) => {
|
|
293
|
-
const socket = net.createConnection(
|
|
377
|
+
const socket = net.createConnection(connectedPath);
|
|
294
378
|
const onError = (error) => {
|
|
295
379
|
socket.destroy();
|
|
296
380
|
reject(error);
|
|
@@ -298,6 +382,8 @@ class NativePtyHostClient extends EventEmitter {
|
|
|
298
382
|
socket.once('error', onError);
|
|
299
383
|
socket.once('connect', () => {
|
|
300
384
|
socket.off('error', onError);
|
|
385
|
+
this.restorePublicSocketPath(connectedPath);
|
|
386
|
+
this.connectedSocketPath = connectedPath;
|
|
301
387
|
this.attachSocket(socket);
|
|
302
388
|
resolve();
|
|
303
389
|
});
|
|
@@ -373,6 +459,9 @@ class NativePtyHostClient extends EventEmitter {
|
|
|
373
459
|
if (error && error.code === 'FARMING_NATIVE_HOST_RUNTIME_MISMATCH') {
|
|
374
460
|
throw error;
|
|
375
461
|
}
|
|
462
|
+
if (error && error.code === 'FARMING_NATIVE_HOST_AMBIGUOUS') {
|
|
463
|
+
throw error;
|
|
464
|
+
}
|
|
376
465
|
if (this.hostStartError) {
|
|
377
466
|
lastError = this.hostStartError;
|
|
378
467
|
}
|
|
@@ -427,7 +516,7 @@ class NativePtyHostClient extends EventEmitter {
|
|
|
427
516
|
return this.controllerIdentityReady;
|
|
428
517
|
}
|
|
429
518
|
|
|
430
|
-
async waitForHostRelease() {
|
|
519
|
+
async waitForHostRelease(privateSocketPath = '') {
|
|
431
520
|
const deadline = Date.now() + this.hostRotationTimeoutMs;
|
|
432
521
|
if (process.platform === 'win32') {
|
|
433
522
|
await delay(Math.min(250, this.hostRotationTimeoutMs));
|
|
@@ -437,7 +526,8 @@ class NativePtyHostClient extends EventEmitter {
|
|
|
437
526
|
const childReleased = !this.hostChild ||
|
|
438
527
|
this.hostChild.exitCode !== null ||
|
|
439
528
|
this.hostChild.signalCode !== null;
|
|
440
|
-
|
|
529
|
+
const privateReleased = !privateSocketPath || !fs.existsSync(privateSocketPath);
|
|
530
|
+
if (!fs.existsSync(this.socketPath) && privateReleased && childReleased) return;
|
|
441
531
|
await delay(50);
|
|
442
532
|
}
|
|
443
533
|
const error = new Error('Timed out waiting for the previous native PTY host to stop');
|
|
@@ -589,6 +679,7 @@ class NativePtyHostClient extends EventEmitter {
|
|
|
589
679
|
throw mismatchError;
|
|
590
680
|
}
|
|
591
681
|
|
|
682
|
+
const privateSocketPath = this.connectedPrivateSocketPath(hostInfo);
|
|
592
683
|
const socket = this.socket;
|
|
593
684
|
if (socket) this.suppressedDisconnectSockets.add(socket);
|
|
594
685
|
let shutdownUncertain = false;
|
|
@@ -622,7 +713,7 @@ class NativePtyHostClient extends EventEmitter {
|
|
|
622
713
|
}
|
|
623
714
|
|
|
624
715
|
try {
|
|
625
|
-
await this.waitForHostRelease();
|
|
716
|
+
await this.waitForHostRelease(privateSocketPath || this.connectedSocketPath);
|
|
626
717
|
} catch (error) {
|
|
627
718
|
if (shutdownUncertain) {
|
|
628
719
|
try {
|
|
@@ -674,6 +765,7 @@ class NativePtyHostClient extends EventEmitter {
|
|
|
674
765
|
this.socket = null;
|
|
675
766
|
this.buffer = '';
|
|
676
767
|
this.connectedHostInfo = null;
|
|
768
|
+
this.connectedSocketPath = '';
|
|
677
769
|
if (this.disposed) return;
|
|
678
770
|
const error = new Error('Native pty host disconnected');
|
|
679
771
|
error.code = 'ECONNRESET';
|
|
@@ -17,6 +17,26 @@ function nativePtyHostSocketPath(configDir) {
|
|
|
17
17
|
return path.join(os.tmpdir(), `farming-native-pty-${process.getuid ? process.getuid() : 'user'}-${hash}.sock`);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function nativePtyHostPrivateSocketHash(socketPath) {
|
|
21
|
+
return crypto.createHash('sha256').update(socketPath).digest('hex').slice(0, 8);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function nativePtyHostPrivateSocketPath(socketPath, options = {}) {
|
|
25
|
+
const pid = Number(options.pid || process.pid);
|
|
26
|
+
const nonce = String(options.nonce || crypto.randomBytes(4).toString('hex'));
|
|
27
|
+
return path.join(
|
|
28
|
+
path.dirname(socketPath),
|
|
29
|
+
`.fpty-${pid}-${nativePtyHostPrivateSocketHash(socketPath)}-${nonce}.sock`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function nativePtyHostPrivateSocketNamePattern(socketPath) {
|
|
34
|
+
const hash = nativePtyHostPrivateSocketHash(socketPath);
|
|
35
|
+
return new RegExp(`^\\.fpty-\\d+-${hash}-[a-f0-9]+\\.sock$`);
|
|
36
|
+
}
|
|
37
|
+
|
|
20
38
|
module.exports = {
|
|
39
|
+
nativePtyHostPrivateSocketNamePattern,
|
|
40
|
+
nativePtyHostPrivateSocketPath,
|
|
21
41
|
nativePtyHostSocketPath,
|
|
22
42
|
};
|
|
@@ -7,7 +7,10 @@ const os = require('os');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
|
|
9
9
|
const TerminalScreenWorkerPool = require('./terminal-screen-worker-pool');
|
|
10
|
-
const {
|
|
10
|
+
const {
|
|
11
|
+
nativePtyHostPrivateSocketPath,
|
|
12
|
+
nativePtyHostSocketPath,
|
|
13
|
+
} = require('./native-pty-host-path');
|
|
11
14
|
const { terminalInputToPtyString } = require('./input-parts');
|
|
12
15
|
const { normalizeInteractiveTerminalEnv } = require('./agent-env');
|
|
13
16
|
const {
|
|
@@ -77,6 +80,15 @@ function sanitizeAgentEnv(env) {
|
|
|
77
80
|
});
|
|
78
81
|
}
|
|
79
82
|
|
|
83
|
+
function socketIdentity(socketPath) {
|
|
84
|
+
const stat = fs.statSync(socketPath, { bigint: true });
|
|
85
|
+
return { dev: stat.dev, ino: stat.ino };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sameSocketIdentity(left, right) {
|
|
89
|
+
return Boolean(left && right && left.dev === right.dev && left.ino === right.ino);
|
|
90
|
+
}
|
|
91
|
+
|
|
80
92
|
class NativePtyHost {
|
|
81
93
|
constructor(options = {}) {
|
|
82
94
|
this.configDir = options.configDir || process.env.FARMING_CONFIG_DIR || path.join(os.homedir(), '.farming');
|
|
@@ -84,6 +96,10 @@ class NativePtyHost {
|
|
|
84
96
|
this.ownerPid = Number(options.ownerPid || process.env.FARMING_NATIVE_PTY_HOST_OWNER_PID || 0);
|
|
85
97
|
this.runtimeIdentity = options.runtimeIdentity || HOST_RUNTIME_IDENTITY;
|
|
86
98
|
this.exitOnShutdown = options.exitOnShutdown !== false;
|
|
99
|
+
this.boundSocketPath = process.platform === 'win32'
|
|
100
|
+
? this.socketPath
|
|
101
|
+
: nativePtyHostPrivateSocketPath(this.socketPath);
|
|
102
|
+
this.boundSocketIdentity = null;
|
|
87
103
|
this.sessions = new Map();
|
|
88
104
|
this.clients = new Set();
|
|
89
105
|
this.ownerCheckTimer = null;
|
|
@@ -130,13 +146,22 @@ class NativePtyHost {
|
|
|
130
146
|
this.server = net.createServer(socket => this.handleConnection(socket));
|
|
131
147
|
await new Promise((resolve, reject) => {
|
|
132
148
|
this.server.once('error', reject);
|
|
133
|
-
this.server.listen(this.
|
|
149
|
+
this.server.listen(this.boundSocketPath, () => {
|
|
134
150
|
this.server.off('error', reject);
|
|
135
151
|
resolve();
|
|
136
152
|
});
|
|
137
153
|
});
|
|
138
154
|
if (process.platform !== 'win32') {
|
|
139
|
-
|
|
155
|
+
this.boundSocketIdentity = socketIdentity(this.boundSocketPath);
|
|
156
|
+
fs.chmodSync(this.boundSocketPath, 0o600);
|
|
157
|
+
try {
|
|
158
|
+
fs.linkSync(this.boundSocketPath, this.socketPath);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
await new Promise(resolve => this.server.close(() => resolve()));
|
|
161
|
+
this.server = null;
|
|
162
|
+
this.boundSocketIdentity = null;
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
140
165
|
}
|
|
141
166
|
console.error(`[${new Date().toISOString()}] Native PTY host listening on ${this.socketPath} ownerPid=${this.ownerPid || ''}`);
|
|
142
167
|
this.startOwnerWatch();
|
|
@@ -173,6 +198,11 @@ class NativePtyHost {
|
|
|
173
198
|
} catch (error) {
|
|
174
199
|
if (!error || error.code !== 'ENOENT') throw error;
|
|
175
200
|
}
|
|
201
|
+
try {
|
|
202
|
+
fs.unlinkSync(this.boundSocketPath);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (!error || error.code !== 'ENOENT') throw error;
|
|
205
|
+
}
|
|
176
206
|
}
|
|
177
207
|
|
|
178
208
|
handleConnection(socket) {
|
|
@@ -317,6 +347,7 @@ class NativePtyHost {
|
|
|
317
347
|
return {
|
|
318
348
|
ok: true,
|
|
319
349
|
pid: process.pid,
|
|
350
|
+
privateSocketPath: process.platform === 'win32' ? '' : this.boundSocketPath,
|
|
320
351
|
runtimeIdentity: this.runtimeIdentity,
|
|
321
352
|
};
|
|
322
353
|
case 'registerController':
|
|
@@ -1476,16 +1507,22 @@ class NativePtyHost {
|
|
|
1476
1507
|
}
|
|
1477
1508
|
}
|
|
1478
1509
|
await this.screenWorkerPool.dispose();
|
|
1479
|
-
if (this.server) {
|
|
1480
|
-
await new Promise(resolve => this.server.close(() => resolve()));
|
|
1481
|
-
this.server = null;
|
|
1482
|
-
}
|
|
1483
|
-
if (process.platform !== 'win32') {
|
|
1510
|
+
if (this.server && process.platform !== 'win32') {
|
|
1484
1511
|
try {
|
|
1485
|
-
|
|
1512
|
+
const currentSocketIdentity = socketIdentity(this.socketPath);
|
|
1513
|
+
if (sameSocketIdentity(currentSocketIdentity, this.boundSocketIdentity)) {
|
|
1514
|
+
// Unlink while this listener is still active. A competing host cannot
|
|
1515
|
+
// replace an active socket between the ownership check and unlink.
|
|
1516
|
+
fs.unlinkSync(this.socketPath);
|
|
1517
|
+
}
|
|
1486
1518
|
} catch (error) {
|
|
1487
1519
|
if (!error || error.code !== 'ENOENT') throw error;
|
|
1488
1520
|
}
|
|
1521
|
+
this.boundSocketIdentity = null;
|
|
1522
|
+
}
|
|
1523
|
+
if (this.server) {
|
|
1524
|
+
await new Promise(resolve => this.server.close(() => resolve()));
|
|
1525
|
+
this.server = null;
|
|
1489
1526
|
}
|
|
1490
1527
|
}
|
|
1491
1528
|
}
|
|
@@ -136,6 +136,23 @@ function assertGlobalWorkspacePathAllowed(agentManager, userPath, options = {})
|
|
|
136
136
|
return { target: requestedTarget, allowedRoots };
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
function assertExactExternalFileReadable(userPath) {
|
|
140
|
+
const requestedTarget = globalUserPathToAbsolute(userPath);
|
|
141
|
+
let target;
|
|
142
|
+
let stat;
|
|
143
|
+
try {
|
|
144
|
+
target = fs.realpathSync(requestedTarget);
|
|
145
|
+
stat = fs.statSync(target);
|
|
146
|
+
fs.accessSync(target, fs.constants.R_OK);
|
|
147
|
+
} catch {
|
|
148
|
+
throw new WorkspaceFileError('external file is not readable', 403);
|
|
149
|
+
}
|
|
150
|
+
if (!stat.isFile()) {
|
|
151
|
+
throw new WorkspaceFileError('external path must be a file', 400);
|
|
152
|
+
}
|
|
153
|
+
return relativeGlobalPath(target);
|
|
154
|
+
}
|
|
155
|
+
|
|
139
156
|
function globalSyntheticTree(agentManager, userPath = '') {
|
|
140
157
|
const { target, allowedRoots } = assertGlobalWorkspacePathAllowed(agentManager, userPath, {
|
|
141
158
|
allowAllowedRootAncestor: true,
|
|
@@ -233,11 +250,14 @@ function createWorkspaceFileRouter(agentManager, fileService, options = {}) {
|
|
|
233
250
|
router.get('/file', async (req, res) => {
|
|
234
251
|
try {
|
|
235
252
|
const rootRef = workspaceRef(req.query);
|
|
236
|
-
|
|
237
|
-
|
|
253
|
+
const requestPath = isGlobalWorkspaceFilesAgentId(rootRef) && req.query.exact === '1'
|
|
254
|
+
? assertExactExternalFileReadable(req.query.path || '')
|
|
255
|
+
: req.query.path || '';
|
|
256
|
+
if (isGlobalWorkspaceFilesAgentId(rootRef) && req.query.exact !== '1') {
|
|
257
|
+
assertGlobalWorkspacePathAllowed(agentManager, requestPath);
|
|
238
258
|
}
|
|
239
259
|
const { root, rootId } = resolveRequestRoot(req.query);
|
|
240
|
-
const file = await fileService.readFile(root,
|
|
260
|
+
const file = await fileService.readFile(root, requestPath, readOptionsForAgent(agentManager, rootRef));
|
|
241
261
|
res.json({ rootId, root, file });
|
|
242
262
|
} catch (error) {
|
|
243
263
|
sendWorkspaceFileError(res, error);
|
|
@@ -247,11 +267,14 @@ function createWorkspaceFileRouter(agentManager, fileService, options = {}) {
|
|
|
247
267
|
router.get('/raw', async (req, res) => {
|
|
248
268
|
try {
|
|
249
269
|
const rootRef = workspaceRef(req.query);
|
|
250
|
-
|
|
251
|
-
|
|
270
|
+
const requestPath = isGlobalWorkspaceFilesAgentId(rootRef) && req.query.exact === '1'
|
|
271
|
+
? assertExactExternalFileReadable(req.query.path || '')
|
|
272
|
+
: req.query.path || '';
|
|
273
|
+
if (isGlobalWorkspaceFilesAgentId(rootRef) && req.query.exact !== '1') {
|
|
274
|
+
assertGlobalWorkspacePathAllowed(agentManager, requestPath);
|
|
252
275
|
}
|
|
253
276
|
const { root } = resolveRequestRoot(req.query);
|
|
254
|
-
const file = await fileService.readPreviewFile(root,
|
|
277
|
+
const file = await fileService.readPreviewFile(root, requestPath, readOptionsForAgent(agentManager, rootRef));
|
|
255
278
|
res
|
|
256
279
|
.status(200)
|
|
257
280
|
.type(file.preview.mediaType)
|
|
@@ -533,6 +556,7 @@ module.exports = {
|
|
|
533
556
|
PROJECT_FILES_WORKSPACE_PREFIX,
|
|
534
557
|
PROJECT_FILES_AGENT_PREFIX: PROJECT_FILES_WORKSPACE_PREFIX,
|
|
535
558
|
assertGlobalWorkspacePathAllowed,
|
|
559
|
+
assertExactExternalFileReadable,
|
|
536
560
|
createWorkspaceFileRouter,
|
|
537
561
|
globalWorkspaceAllowedRoots,
|
|
538
562
|
isGlobalWorkspaceFilesAgentId,
|