@kortix/agent-tunnel 0.1.0 → 0.1.2
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/package.json +4 -2
- package/src/agent/agent.ts +72 -39
- package/src/agent/capabilities/desktop/atspi-helper.ts +1 -1
- package/src/agent/capabilities/desktop/csharp-helper.ts +1 -1
- package/src/agent/capabilities/desktop/linux-driver.ts +1 -1
- package/src/agent/capabilities/desktop/macos-driver.ts +1 -1
- package/src/agent/capabilities/desktop/swift-helper.ts +1 -1
- package/src/agent/capabilities/filesystem.ts +7 -7
- package/src/agent/capabilities/shell.ts +29 -19
- package/src/agent/cli.ts +156 -22
- package/src/agent/config.ts +21 -1
- package/src/agent/security/command-validator.ts +7 -39
- package/src/agent/security/path-validator.ts +16 -19
- package/src/agent/security/permission-guard.ts +2 -1
- package/src/client/cli.test.ts +758 -0
- package/src/client/cli.ts +716 -0
- package/src/client/index.ts +1 -0
- package/src/index.ts +2 -0
- package/src/server/relay.ts +50 -6
- package/src/server/routes.ts +15 -1
- package/src/server/server.ts +22 -22
- package/src/server/ws-handler.ts +76 -12
- package/src/shared/crypto.ts +2 -3
- package/src/shared/index.ts +2 -0
- package/src/shared/types.ts +30 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kortix/agent-tunnel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Tunnel relay between cloud AI agents and local machines — server relay, local agent, client SDK, JSON-RPC, HMAC signing",
|
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
"./shared": "./src/shared/index.ts",
|
|
12
12
|
"./server": "./src/server/index.ts",
|
|
13
13
|
"./client": "./src/client/index.ts",
|
|
14
|
+
"./client/cli": "./src/client/cli.ts",
|
|
14
15
|
"./agent": "./src/agent/index.ts"
|
|
15
16
|
},
|
|
16
17
|
"bin": {
|
|
17
|
-
"agent-tunnel": "src/agent/cli.ts"
|
|
18
|
+
"agent-tunnel": "src/agent/cli.ts",
|
|
19
|
+
"agent-tunnel-cli": "src/client/cli.ts"
|
|
18
20
|
},
|
|
19
21
|
"files": [
|
|
20
22
|
"src/"
|
package/src/agent/agent.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { TunnelConfig } from './config';
|
|
|
3
3
|
import { CapabilityRegistry, type RpcHandler } from './capabilities/index';
|
|
4
4
|
import { PermissionGuard } from './security/permission-guard';
|
|
5
5
|
import type { LocalPermission } from './security/permission-guard';
|
|
6
|
-
import {
|
|
6
|
+
import { signMessage, verifyMessageSignature } from '../shared/crypto';
|
|
7
7
|
|
|
8
8
|
interface JsonRpcRequest {
|
|
9
9
|
jsonrpc: '2.0';
|
|
@@ -54,14 +54,14 @@ export class TunnelAgent {
|
|
|
54
54
|
private uptimeInterval: ReturnType<typeof setInterval> | null = null;
|
|
55
55
|
|
|
56
56
|
// HMAC signature verification
|
|
57
|
-
private signingKey: string;
|
|
57
|
+
private signingKey: string | null = null;
|
|
58
58
|
private lastNonce = 0;
|
|
59
|
+
private responseNonce = 0;
|
|
59
60
|
|
|
60
61
|
constructor(config: TunnelConfig, registry: CapabilityRegistry) {
|
|
61
62
|
this.config = config;
|
|
62
63
|
this.registry = registry;
|
|
63
64
|
this.permissionGuard = new PermissionGuard();
|
|
64
|
-
this.signingKey = deriveSigningKey(config.token);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
connect(): void {
|
|
@@ -113,10 +113,13 @@ export class TunnelAgent {
|
|
|
113
113
|
this.ws.addEventListener('open', () => {
|
|
114
114
|
this.reconnectAttempts = 0;
|
|
115
115
|
this.uptime = 0;
|
|
116
|
-
this.lastNonce = 0;
|
|
116
|
+
this.lastNonce = 0;
|
|
117
|
+
this.responseNonce = 0;
|
|
118
|
+
this.signingKey = null;
|
|
117
119
|
this.uptimeInterval = setInterval(() => { this.uptime++; }, 1000);
|
|
118
120
|
|
|
119
|
-
|
|
121
|
+
// Send auth handshake as first message (token never in URL)
|
|
122
|
+
this.send({ type: 'auth', token: this.config.token });
|
|
120
123
|
});
|
|
121
124
|
|
|
122
125
|
this.ws.addEventListener('message', (event) => {
|
|
@@ -130,6 +133,10 @@ export class TunnelAgent {
|
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
if (!this.isShuttingDown) {
|
|
136
|
+
if (event.code === 4001) {
|
|
137
|
+
log(`${c.red}✗${c.reset}`, `Authentication failed — check your token`);
|
|
138
|
+
return; // Don't reconnect on auth failure
|
|
139
|
+
}
|
|
133
140
|
log(`${c.yellow}○${c.reset}`, `Disconnected ${c.gray}(code: ${event.code})${c.reset}`);
|
|
134
141
|
this.scheduleReconnect();
|
|
135
142
|
}
|
|
@@ -141,7 +148,7 @@ export class TunnelAgent {
|
|
|
141
148
|
}
|
|
142
149
|
|
|
143
150
|
private async handleMessage(raw: string): Promise<void> {
|
|
144
|
-
let msg:
|
|
151
|
+
let msg: any;
|
|
145
152
|
try {
|
|
146
153
|
msg = JSON.parse(raw);
|
|
147
154
|
} catch {
|
|
@@ -149,20 +156,31 @@ export class TunnelAgent {
|
|
|
149
156
|
return;
|
|
150
157
|
}
|
|
151
158
|
|
|
152
|
-
//
|
|
153
|
-
if ('
|
|
154
|
-
this.
|
|
159
|
+
// Handle auth_ok — server sends signing key after successful auth
|
|
160
|
+
if (msg.type === 'auth_ok' && msg.signingKey) {
|
|
161
|
+
this.signingKey = msg.signingKey;
|
|
162
|
+
log(`${c.green}●${c.reset}`, `Connected ${c.reset}${c.gray}(${this.registry.getCapabilityNames().join(', ')})${c.reset}`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!this.signingKey) {
|
|
167
|
+
log(`${c.yellow}!${c.reset}`, `Message received before auth completed`);
|
|
155
168
|
return;
|
|
156
169
|
}
|
|
157
170
|
|
|
158
|
-
// ── Verify HMAC signature on all other messages ─────────────────
|
|
159
171
|
if (!this.verifyIncomingSignature(msg, raw)) {
|
|
160
172
|
if ('id' in msg && msg.id) {
|
|
161
|
-
this.
|
|
173
|
+
this.sendSignedError(msg.id, -32000, 'Invalid message signature');
|
|
162
174
|
}
|
|
163
175
|
return;
|
|
164
176
|
}
|
|
165
177
|
|
|
178
|
+
// ── Heartbeat ping (signature verified above) ────────────────────
|
|
179
|
+
if ('method' in msg && msg.method === 'tunnel.ping') {
|
|
180
|
+
this.sendPong();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
166
184
|
// ── Permission sync notification ────────────────────────────────
|
|
167
185
|
if ('method' in msg && msg.method === 'tunnel.permissions.sync') {
|
|
168
186
|
const permissions = (msg.params?.permissions || []) as LocalPermission[];
|
|
@@ -194,8 +212,6 @@ export class TunnelAgent {
|
|
|
194
212
|
// ── Token rotation notification ─────────────────────────────────
|
|
195
213
|
if ('method' in msg && msg.method === 'tunnel.token.rotated') {
|
|
196
214
|
log(`${c.yellow}!${c.reset}`, `Token rotated — reconnecting with new token`);
|
|
197
|
-
// The server will close the WS shortly; the reconnect logic handles the rest.
|
|
198
|
-
// Caller must update config.token before reconnect succeeds.
|
|
199
215
|
return;
|
|
200
216
|
}
|
|
201
217
|
|
|
@@ -208,7 +224,6 @@ export class TunnelAgent {
|
|
|
208
224
|
|
|
209
225
|
/**
|
|
210
226
|
* Verify HMAC signature on incoming messages (excluding pings).
|
|
211
|
-
* Returns true if valid, false if signature check fails.
|
|
212
227
|
*/
|
|
213
228
|
private verifyIncomingSignature(msg: IncomingMessage, _raw: string): boolean {
|
|
214
229
|
const sig = (msg as any)._sig as string | undefined;
|
|
@@ -219,17 +234,15 @@ export class TunnelAgent {
|
|
|
219
234
|
return false;
|
|
220
235
|
}
|
|
221
236
|
|
|
222
|
-
// Replay protection: nonce must be strictly increasing
|
|
223
237
|
if (nonce <= this.lastNonce) {
|
|
224
238
|
log(`${c.red}✗${c.reset}`, `Replay detected: nonce ${nonce} <= ${this.lastNonce}`);
|
|
225
239
|
return false;
|
|
226
240
|
}
|
|
227
241
|
|
|
228
|
-
// Build the payload to verify (message without _sig and _nonce)
|
|
229
242
|
const { _sig, _nonce, ...payloadObj } = msg as any;
|
|
230
243
|
const payload = JSON.stringify(payloadObj);
|
|
231
244
|
|
|
232
|
-
if (!verifyMessageSignature(this.signingKey
|
|
245
|
+
if (!verifyMessageSignature(this.signingKey!, payload, nonce, sig)) {
|
|
233
246
|
log(`${c.red}✗${c.reset}`, `Invalid HMAC signature`);
|
|
234
247
|
return false;
|
|
235
248
|
}
|
|
@@ -241,38 +254,65 @@ export class TunnelAgent {
|
|
|
241
254
|
private async handleRpcRequest(request: JsonRpcRequest): Promise<void> {
|
|
242
255
|
const { id, method, params = {} } = request;
|
|
243
256
|
|
|
244
|
-
// ── Permission enforcement (defense-in-depth) ───────────────────
|
|
245
257
|
const permissionId = params.permissionId as string | undefined;
|
|
246
258
|
if (!this.permissionGuard.checkPermission(permissionId)) {
|
|
247
|
-
this.
|
|
259
|
+
this.sendSignedError(id, -32000, `Permission denied: ${permissionId ? 'invalid or expired permission' : 'no permissionId provided'}`);
|
|
248
260
|
return;
|
|
249
261
|
}
|
|
250
262
|
|
|
251
263
|
const handler = this.registry.getHandler(method);
|
|
252
264
|
if (!handler) {
|
|
253
|
-
this.
|
|
265
|
+
this.sendSignedError(id, -32001, `Capability not registered for method: ${method}`);
|
|
254
266
|
return;
|
|
255
267
|
}
|
|
256
268
|
|
|
257
269
|
try {
|
|
258
270
|
const result = await handler(params);
|
|
259
|
-
this.
|
|
271
|
+
this.sendSignedResult(id, result);
|
|
260
272
|
} catch (err) {
|
|
261
273
|
const message = err instanceof Error ? err.message : String(err);
|
|
262
|
-
this.
|
|
274
|
+
this.sendSignedError(id, -32003, message);
|
|
263
275
|
}
|
|
264
276
|
}
|
|
265
277
|
|
|
266
|
-
|
|
267
|
-
|
|
278
|
+
/** Send HMAC-signed RPC result. */
|
|
279
|
+
private sendSignedResult(id: string, result: unknown): void {
|
|
280
|
+
const data = { jsonrpc: '2.0' as const, id, result };
|
|
281
|
+
this.sendSigned(data);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Send HMAC-signed RPC error. */
|
|
285
|
+
private sendSignedError(id: string, code: number, message: string): void {
|
|
286
|
+
const data = { jsonrpc: '2.0' as const, id, error: { code, message } };
|
|
287
|
+
this.sendSigned(data);
|
|
268
288
|
}
|
|
269
289
|
|
|
270
|
-
private
|
|
271
|
-
this.
|
|
290
|
+
private sendSigned(data: Record<string, unknown>): void {
|
|
291
|
+
if (this.ws?.readyState === WebSocket.OPEN && this.signingKey) {
|
|
292
|
+
const nonce = ++this.responseNonce;
|
|
293
|
+
const payload = JSON.stringify(data);
|
|
294
|
+
const sig = signMessage(this.signingKey, payload, nonce);
|
|
295
|
+
const signed = { ...data, _sig: sig, _nonce: nonce };
|
|
296
|
+
try {
|
|
297
|
+
this.ws.send(JSON.stringify(signed));
|
|
298
|
+
} catch (err) {
|
|
299
|
+
log(`${c.red}✗${c.reset}`, `Send failed`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private send(data: unknown): void {
|
|
305
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
306
|
+
try {
|
|
307
|
+
this.ws.send(JSON.stringify(data));
|
|
308
|
+
} catch (err) {
|
|
309
|
+
log(`${c.red}✗${c.reset}`, `Send failed`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
272
312
|
}
|
|
273
313
|
|
|
274
314
|
private sendPong(): void {
|
|
275
|
-
this.
|
|
315
|
+
this.sendSigned({
|
|
276
316
|
jsonrpc: '2.0',
|
|
277
317
|
method: 'tunnel.pong',
|
|
278
318
|
params: {
|
|
@@ -283,22 +323,12 @@ export class TunnelAgent {
|
|
|
283
323
|
platform: platform(),
|
|
284
324
|
arch: arch(),
|
|
285
325
|
osVersion: release(),
|
|
286
|
-
agentVersion: '0.1.
|
|
326
|
+
agentVersion: '0.1.2',
|
|
287
327
|
},
|
|
288
328
|
},
|
|
289
329
|
});
|
|
290
330
|
}
|
|
291
331
|
|
|
292
|
-
private send(data: unknown): void {
|
|
293
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
294
|
-
try {
|
|
295
|
-
this.ws.send(JSON.stringify(data));
|
|
296
|
-
} catch (err) {
|
|
297
|
-
log(`${c.red}✗${c.reset}`, `Send failed`);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
332
|
private scheduleReconnect(): void {
|
|
303
333
|
if (this.isShuttingDown) return;
|
|
304
334
|
|
|
@@ -320,9 +350,12 @@ export class TunnelAgent {
|
|
|
320
350
|
.replace(/^http:/, 'ws:')
|
|
321
351
|
.replace(/^https:/, 'wss:');
|
|
322
352
|
|
|
353
|
+
if (base.startsWith('ws://') && !base.includes('localhost') && !base.includes('127.0.0.1')) {
|
|
354
|
+
log(`${c.red}!${c.reset}`, `${c.red}WARNING: Connecting over unencrypted ws:// to a remote host. Token will be sent in plaintext. Use https:// API URL for production.${c.reset}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
323
357
|
const wsPath = this.config.wsPath || '/ws';
|
|
324
358
|
const params = new URLSearchParams({
|
|
325
|
-
token: this.config.token,
|
|
326
359
|
tunnelId: this.config.tunnelId,
|
|
327
360
|
});
|
|
328
361
|
|
|
@@ -4,7 +4,7 @@ import { join } from 'path';
|
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
|
|
6
6
|
const HELPER_VERSION = 'v1';
|
|
7
|
-
const BIN_DIR = join(homedir(), '.
|
|
7
|
+
const BIN_DIR = join(homedir(), '.agent-tunnel', 'bin');
|
|
8
8
|
const HELPER_PATH = join(BIN_DIR, `atspi-helper-${HELPER_VERSION}.py`);
|
|
9
9
|
|
|
10
10
|
const PYTHON_SOURCE = `#!/usr/bin/env python3
|
|
@@ -4,7 +4,7 @@ import { join } from 'path';
|
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
|
|
6
6
|
const HELPER_VERSION = 'v1';
|
|
7
|
-
const BIN_DIR = join(homedir(), '.
|
|
7
|
+
const BIN_DIR = join(homedir(), '.agent-tunnel', 'bin');
|
|
8
8
|
const HELPER_PATH = join(BIN_DIR, `desktop-helper-win-${HELPER_VERSION}.exe`);
|
|
9
9
|
|
|
10
10
|
const CSHARP_SOURCE = `
|
|
@@ -32,7 +32,7 @@ import type {
|
|
|
32
32
|
} from './types';
|
|
33
33
|
|
|
34
34
|
function tmpPath(): string {
|
|
35
|
-
return join(tmpdir(), `
|
|
35
|
+
return join(tmpdir(), `tunnel-ss-${randomBytes(6).toString('hex')}.png`);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function exec(cmd: string, args: string[]): Promise<string> {
|
|
@@ -32,7 +32,7 @@ import type {
|
|
|
32
32
|
} from './types';
|
|
33
33
|
|
|
34
34
|
function tmpPath(ext: string = '.png'): string {
|
|
35
|
-
return join(tmpdir(), `
|
|
35
|
+
return join(tmpdir(), `tunnel-ss-${randomBytes(6).toString('hex')}${ext}`);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function exec(cmd: string, args: string[], timeoutMs = 15000): Promise<string> {
|
|
@@ -4,7 +4,7 @@ import { join } from 'path';
|
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
|
|
6
6
|
const HELPER_VERSION = 'v4';
|
|
7
|
-
const BIN_DIR = join(homedir(), '.
|
|
7
|
+
const BIN_DIR = join(homedir(), '.agent-tunnel', 'bin');
|
|
8
8
|
const HELPER_PATH = join(BIN_DIR, `desktop-helper-${HELPER_VERSION}`);
|
|
9
9
|
|
|
10
10
|
const SWIFT_SOURCE = `
|
|
@@ -18,15 +18,15 @@ export function createFilesystemCapability(config: TunnelConfig): Capability {
|
|
|
18
18
|
const path = params.path as string;
|
|
19
19
|
const encoding = (params.encoding as BufferEncoding) || 'utf-8';
|
|
20
20
|
|
|
21
|
-
validatePath(path, config.allowedPaths);
|
|
21
|
+
validatePath(path, config.allowedPaths, config.blockedPaths);
|
|
22
22
|
|
|
23
|
-
const content = await readFile(path, { encoding });
|
|
24
23
|
const stats = await stat(path);
|
|
25
|
-
|
|
26
24
|
if (stats.size > config.maxFileSize) {
|
|
27
25
|
throw new Error(`File exceeds max size (${stats.size} > ${config.maxFileSize})`);
|
|
28
26
|
}
|
|
29
27
|
|
|
28
|
+
const content = await readFile(path, { encoding });
|
|
29
|
+
|
|
30
30
|
return {
|
|
31
31
|
content,
|
|
32
32
|
size: stats.size,
|
|
@@ -40,7 +40,7 @@ export function createFilesystemCapability(config: TunnelConfig): Capability {
|
|
|
40
40
|
const content = params.content as string;
|
|
41
41
|
const encoding = (params.encoding as BufferEncoding) || 'utf-8';
|
|
42
42
|
|
|
43
|
-
validatePath(path, config.allowedPaths);
|
|
43
|
+
validatePath(path, config.allowedPaths, config.blockedPaths);
|
|
44
44
|
|
|
45
45
|
if (content.length > config.maxFileSize) {
|
|
46
46
|
throw new Error(`Content exceeds max size (${content.length} > ${config.maxFileSize})`);
|
|
@@ -62,7 +62,7 @@ export function createFilesystemCapability(config: TunnelConfig): Capability {
|
|
|
62
62
|
const path = params.path as string;
|
|
63
63
|
const recursive = params.recursive as boolean || false;
|
|
64
64
|
|
|
65
|
-
validatePath(path, config.allowedPaths);
|
|
65
|
+
validatePath(path, config.allowedPaths, config.blockedPaths);
|
|
66
66
|
|
|
67
67
|
const entries = await readdir(path, { withFileTypes: true });
|
|
68
68
|
|
|
@@ -100,7 +100,7 @@ export function createFilesystemCapability(config: TunnelConfig): Capability {
|
|
|
100
100
|
methods.set('fs.stat', async (params) => {
|
|
101
101
|
const path = params.path as string;
|
|
102
102
|
|
|
103
|
-
validatePath(path, config.allowedPaths);
|
|
103
|
+
validatePath(path, config.allowedPaths, config.blockedPaths);
|
|
104
104
|
|
|
105
105
|
const stats = await stat(path);
|
|
106
106
|
|
|
@@ -119,7 +119,7 @@ export function createFilesystemCapability(config: TunnelConfig): Capability {
|
|
|
119
119
|
methods.set('fs.delete', async (params) => {
|
|
120
120
|
const path = params.path as string;
|
|
121
121
|
|
|
122
|
-
validatePath(path, config.allowedPaths);
|
|
122
|
+
validatePath(path, config.allowedPaths, config.blockedPaths);
|
|
123
123
|
|
|
124
124
|
await unlink(path);
|
|
125
125
|
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Security:
|
|
5
5
|
* - Commands are executed as array args (no shell interpolation)
|
|
6
|
-
* - First arg (executable) is validated against allowedCommands
|
|
7
|
-
* - Working directory is validated against allowedPaths
|
|
6
|
+
* - First arg (executable) is validated against allowedCommands / blockedCommands
|
|
7
|
+
* - Working directory is validated against allowedPaths / blockedPaths
|
|
8
8
|
* - Timeout enforcement
|
|
9
9
|
*/
|
|
10
10
|
|
|
@@ -14,9 +14,6 @@ import { validateCommand } from '../security/command-validator';
|
|
|
14
14
|
import { validatePath } from '../security/path-validator';
|
|
15
15
|
import type { TunnelConfig } from '../config';
|
|
16
16
|
|
|
17
|
-
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
18
|
-
const MAX_OUTPUT_SIZE = 1024 * 1024;
|
|
19
|
-
|
|
20
17
|
export function createShellCapability(config: TunnelConfig): Capability {
|
|
21
18
|
const methods = new Map<string, RpcHandler>();
|
|
22
19
|
|
|
@@ -25,19 +22,18 @@ export function createShellCapability(config: TunnelConfig): Capability {
|
|
|
25
22
|
const args = (params.args as string[]) || [];
|
|
26
23
|
const cwd = (params.cwd as string) || config.workingDir;
|
|
27
24
|
const timeout = Math.min(
|
|
28
|
-
(params.timeout as number) ||
|
|
29
|
-
|
|
25
|
+
(params.timeout as number) || config.shellTimeout,
|
|
26
|
+
config.shellMaxTimeout,
|
|
30
27
|
);
|
|
31
28
|
|
|
32
|
-
validateCommand(command, config.allowedCommands);
|
|
29
|
+
validateCommand(command, config.allowedCommands, config.blockedCommands);
|
|
33
30
|
|
|
34
31
|
if (cwd) {
|
|
35
|
-
validatePath(cwd, config.allowedPaths);
|
|
32
|
+
validatePath(cwd, config.allowedPaths, config.blockedPaths);
|
|
36
33
|
}
|
|
37
34
|
|
|
38
|
-
const SAFE_ENV_KEYS = ['PATH', 'HOME', 'USER', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TMPDIR', 'NODE_ENV', 'HOSTNAME'];
|
|
39
35
|
const safeEnv: Record<string, string> = { TERM: 'dumb' };
|
|
40
|
-
for (const key of
|
|
36
|
+
for (const key of config.shellEnvPassthrough) {
|
|
41
37
|
if (process.env[key]) {
|
|
42
38
|
safeEnv[key] = process.env[key]!;
|
|
43
39
|
}
|
|
@@ -57,18 +53,32 @@ export function createShellCapability(config: TunnelConfig): Capability {
|
|
|
57
53
|
let stderrTruncated = false;
|
|
58
54
|
|
|
59
55
|
proc.stdout?.on('data', (data: Buffer) => {
|
|
60
|
-
if (stdout.length
|
|
61
|
-
stdout += data.toString();
|
|
62
|
-
} else {
|
|
56
|
+
if (stdout.length >= config.shellMaxOutputSize) {
|
|
63
57
|
stdoutTruncated = true;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const chunk = data.toString();
|
|
61
|
+
const remaining = config.shellMaxOutputSize - stdout.length;
|
|
62
|
+
if (chunk.length > remaining) {
|
|
63
|
+
stdout += chunk.slice(0, remaining);
|
|
64
|
+
stdoutTruncated = true;
|
|
65
|
+
} else {
|
|
66
|
+
stdout += chunk;
|
|
64
67
|
}
|
|
65
68
|
});
|
|
66
69
|
|
|
67
70
|
proc.stderr?.on('data', (data: Buffer) => {
|
|
68
|
-
if (stderr.length
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
if (stderr.length >= config.shellMaxOutputSize) {
|
|
72
|
+
stderrTruncated = true;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const chunk = data.toString();
|
|
76
|
+
const remaining = config.shellMaxOutputSize - stderr.length;
|
|
77
|
+
if (chunk.length > remaining) {
|
|
78
|
+
stderr += chunk.slice(0, remaining);
|
|
71
79
|
stderrTruncated = true;
|
|
80
|
+
} else {
|
|
81
|
+
stderr += chunk;
|
|
72
82
|
}
|
|
73
83
|
});
|
|
74
84
|
|
|
@@ -80,8 +90,8 @@ export function createShellCapability(config: TunnelConfig): Capability {
|
|
|
80
90
|
resolve({
|
|
81
91
|
exitCode: code,
|
|
82
92
|
signal,
|
|
83
|
-
stdout
|
|
84
|
-
stderr
|
|
93
|
+
stdout,
|
|
94
|
+
stderr,
|
|
85
95
|
stdoutTruncated,
|
|
86
96
|
stderrTruncated,
|
|
87
97
|
});
|