@kortix/agent-tunnel 0.12.7 → 0.13.0
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/dist/agent-cli.js +1191 -887
- package/package.json +1 -1
- package/src/agent/agent.ts +53 -29
- package/src/agent/banner.ts +82 -0
- package/src/agent/cli-help.test.ts +79 -15
- package/src/agent/cli-reauth.test.ts +205 -0
- package/src/agent/cli.ts +399 -661
- package/src/agent/config.ts +19 -0
- package/src/agent/credential-probe.ts +95 -0
- package/src/agent/credential-store.ts +74 -0
- package/src/agent/device-auth.test.ts +132 -0
- package/src/agent/device-auth.ts +213 -0
- package/src/agent/log-format.ts +24 -0
- package/src/agent/prompts.ts +37 -0
- package/src/agent/service-control.ts +69 -0
- package/src/agent/service-drivers.ts +299 -0
- package/src/agent/service-lifecycle.test.ts +248 -0
- package/src/agent/service-paths.ts +80 -0
- package/src/agent/service-quoting.ts +16 -0
- package/src/agent/service.test.ts +52 -2
- package/src/agent/service.ts +132 -356
- package/src/agent/terminal.ts +53 -0
- package/src/agent/version.ts +52 -0
package/dist/agent-cli.js
CHANGED
|
@@ -2968,6 +2968,14 @@ if (typeof globalThis.WebSocket === "undefined") {
|
|
|
2968
2968
|
}
|
|
2969
2969
|
}
|
|
2970
2970
|
|
|
2971
|
+
// src/agent/cli.ts
|
|
2972
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
2973
|
+
import { hostname as hostname4 } from "os";
|
|
2974
|
+
import { join as join10 } from "path";
|
|
2975
|
+
|
|
2976
|
+
// src/agent/agent.ts
|
|
2977
|
+
import { hostname, platform, arch, release } from "os";
|
|
2978
|
+
|
|
2971
2979
|
// src/agent/config.ts
|
|
2972
2980
|
import { chmodSync, existsSync, lstatSync, readFileSync } from "fs";
|
|
2973
2981
|
import { join } from "path";
|
|
@@ -3402,6 +3410,14 @@ function absoluteWsPath(value) {
|
|
|
3402
3410
|
}
|
|
3403
3411
|
return value;
|
|
3404
3412
|
}
|
|
3413
|
+
function buildTunnelWsUrl(config) {
|
|
3414
|
+
const base = trustedHttpUrl(config.apiUrl).replace(/^http:/, "ws:").replace(/^https:/, "wss:");
|
|
3415
|
+
const wsPath = absoluteWsPath(config.wsPath || "/ws");
|
|
3416
|
+
const params = new URLSearchParams({
|
|
3417
|
+
tunnelId: trustedCredential(config.tunnelId, "tunnelId")
|
|
3418
|
+
});
|
|
3419
|
+
return `${base}${wsPath}?${params.toString()}`;
|
|
3420
|
+
}
|
|
3405
3421
|
function loadConfig(overrides = {}) {
|
|
3406
3422
|
let fileConfig = {};
|
|
3407
3423
|
if (existsSync(CONFIG_FILE)) {
|
|
@@ -3442,8 +3458,78 @@ function loadConfig(overrides = {}) {
|
|
|
3442
3458
|
return merged;
|
|
3443
3459
|
}
|
|
3444
3460
|
|
|
3445
|
-
// src/agent/
|
|
3446
|
-
import {
|
|
3461
|
+
// src/agent/version.ts
|
|
3462
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
3463
|
+
import { dirname, join as join2 } from "path";
|
|
3464
|
+
import { fileURLToPath } from "url";
|
|
3465
|
+
var PACKAGE_NAME = "@kortix/agent-tunnel";
|
|
3466
|
+
var MAX_LOOKUP_DEPTH = 8;
|
|
3467
|
+
var cached = null;
|
|
3468
|
+
function moduleDirectory() {
|
|
3469
|
+
try {
|
|
3470
|
+
return dirname(fileURLToPath(import.meta.url));
|
|
3471
|
+
} catch {
|
|
3472
|
+
return process.cwd();
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
function agentTunnelVersion() {
|
|
3476
|
+
if (cached !== null)
|
|
3477
|
+
return cached;
|
|
3478
|
+
let directory = moduleDirectory();
|
|
3479
|
+
for (let depth = 0;depth < MAX_LOOKUP_DEPTH; depth++) {
|
|
3480
|
+
try {
|
|
3481
|
+
const manifest = JSON.parse(readFileSync2(join2(directory, "package.json"), "utf8"));
|
|
3482
|
+
if (manifest?.name === PACKAGE_NAME && typeof manifest.version === "string") {
|
|
3483
|
+
const version = manifest.version;
|
|
3484
|
+
cached = version;
|
|
3485
|
+
return version;
|
|
3486
|
+
}
|
|
3487
|
+
} catch {}
|
|
3488
|
+
const parent = dirname(directory);
|
|
3489
|
+
if (parent === directory)
|
|
3490
|
+
break;
|
|
3491
|
+
directory = parent;
|
|
3492
|
+
}
|
|
3493
|
+
cached = "unknown";
|
|
3494
|
+
return cached;
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
// src/agent/terminal.ts
|
|
3498
|
+
var c = {
|
|
3499
|
+
reset: "\x1B[0m",
|
|
3500
|
+
bold: "\x1B[1m",
|
|
3501
|
+
dim: "\x1B[2m",
|
|
3502
|
+
cyan: "\x1B[36m",
|
|
3503
|
+
green: "\x1B[32m",
|
|
3504
|
+
yellow: "\x1B[33m",
|
|
3505
|
+
red: "\x1B[31m",
|
|
3506
|
+
white: "\x1B[97m",
|
|
3507
|
+
gray: "\x1B[90m"
|
|
3508
|
+
};
|
|
3509
|
+
var ANSI = /\x1b\[[0-9;]*m/g;
|
|
3510
|
+
function stripAnsi(value) {
|
|
3511
|
+
return value.replace(ANSI, "");
|
|
3512
|
+
}
|
|
3513
|
+
function visibleLength(value) {
|
|
3514
|
+
return stripAnsi(value).length;
|
|
3515
|
+
}
|
|
3516
|
+
function clearScreen() {
|
|
3517
|
+
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
3518
|
+
}
|
|
3519
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3520
|
+
var glyph = {
|
|
3521
|
+
on: `${c.green}●${c.reset}`,
|
|
3522
|
+
off: `${c.gray}○${c.reset}`,
|
|
3523
|
+
warn: `${c.yellow}!${c.reset}`,
|
|
3524
|
+
bad: `${c.red}✗${c.reset}`,
|
|
3525
|
+
mark: `${c.cyan}◆${c.reset}`
|
|
3526
|
+
};
|
|
3527
|
+
function field(label, value, width = 14) {
|
|
3528
|
+
console.log(` ${c.dim}${label.padEnd(width)}${c.reset}${value}`);
|
|
3529
|
+
}
|
|
3530
|
+
function blankLine() {
|
|
3531
|
+
console.log("");
|
|
3532
|
+
}
|
|
3447
3533
|
|
|
3448
3534
|
// src/agent/security/permission-guard.ts
|
|
3449
3535
|
class PermissionGuard {
|
|
@@ -3518,19 +3604,10 @@ function verifyMessageSignature(signingKey, payload, nonce, signature) {
|
|
|
3518
3604
|
}
|
|
3519
3605
|
|
|
3520
3606
|
// src/agent/agent.ts
|
|
3521
|
-
var AGENT_VERSION =
|
|
3607
|
+
var AGENT_VERSION = agentTunnelVersion();
|
|
3608
|
+
var AUTH_REJECTED_CLOSE_CODES = [4001, 4003];
|
|
3609
|
+
var AGENT_REPLACED_CLOSE_CODE = 4004;
|
|
3522
3610
|
var MAX_RPC_MESSAGE_SIZE = 5 * 1024 * 1024;
|
|
3523
|
-
var c = {
|
|
3524
|
-
reset: "\x1B[0m",
|
|
3525
|
-
bold: "\x1B[1m",
|
|
3526
|
-
dim: "\x1B[2m",
|
|
3527
|
-
cyan: "\x1B[36m",
|
|
3528
|
-
green: "\x1B[32m",
|
|
3529
|
-
yellow: "\x1B[33m",
|
|
3530
|
-
red: "\x1B[31m",
|
|
3531
|
-
white: "\x1B[97m",
|
|
3532
|
-
gray: "\x1B[90m"
|
|
3533
|
-
};
|
|
3534
3611
|
function log(icon, msg) {
|
|
3535
3612
|
const safeIcon = icon.replace(/[\r\n]/g, " ");
|
|
3536
3613
|
const safeMsg = msg.replace(/[\r\n]/g, " ");
|
|
@@ -3539,6 +3616,7 @@ function log(icon, msg) {
|
|
|
3539
3616
|
}
|
|
3540
3617
|
|
|
3541
3618
|
class TunnelAgent {
|
|
3619
|
+
hooks;
|
|
3542
3620
|
ws = null;
|
|
3543
3621
|
registry;
|
|
3544
3622
|
permissionGuard;
|
|
@@ -3554,10 +3632,11 @@ class TunnelAgent {
|
|
|
3554
3632
|
signingKey = null;
|
|
3555
3633
|
lastNonce = 0;
|
|
3556
3634
|
responseNonce = 0;
|
|
3557
|
-
constructor(config, registry) {
|
|
3635
|
+
constructor(config, registry, hooks = {}) {
|
|
3558
3636
|
this.config = config;
|
|
3559
3637
|
this.registry = registry;
|
|
3560
3638
|
this.permissionGuard = new PermissionGuard;
|
|
3639
|
+
this.hooks = hooks;
|
|
3561
3640
|
}
|
|
3562
3641
|
connect() {
|
|
3563
3642
|
if (this.ws) {
|
|
@@ -3627,16 +3706,21 @@ class TunnelAgent {
|
|
|
3627
3706
|
}
|
|
3628
3707
|
if (!this.isShuttingDown) {
|
|
3629
3708
|
if (event.code === 4001) {
|
|
3630
|
-
|
|
3709
|
+
this.isShuttingDown = true;
|
|
3710
|
+
log(`${c.red}✗${c.reset}`, `Credential rejected — run \`agent-tunnel connect --reauth\` to pair again`);
|
|
3711
|
+
this.hooks.onTerminalClose?.({ code: event.code, reason: "credential-rejected" });
|
|
3631
3712
|
return;
|
|
3632
3713
|
}
|
|
3633
3714
|
if (event.code === 4003) {
|
|
3634
|
-
|
|
3715
|
+
this.isShuttingDown = true;
|
|
3716
|
+
log(`${c.red}✗${c.reset}`, `Device credential was revoked — run \`agent-tunnel connect --reauth\` to pair again`);
|
|
3717
|
+
this.hooks.onTerminalClose?.({ code: event.code, reason: "credential-rejected" });
|
|
3635
3718
|
return;
|
|
3636
3719
|
}
|
|
3637
|
-
if (event.code ===
|
|
3720
|
+
if (event.code === AGENT_REPLACED_CLOSE_CODE) {
|
|
3638
3721
|
this.isShuttingDown = true;
|
|
3639
3722
|
log(`${c.yellow}○${c.reset}`, `Another Agent Tunnel process connected with these credentials — stopping this process`);
|
|
3723
|
+
this.hooks.onTerminalClose?.({ code: event.code, reason: "replaced" });
|
|
3640
3724
|
return;
|
|
3641
3725
|
}
|
|
3642
3726
|
log(`${c.yellow}○${c.reset}`, `Disconnected ${c.gray}(code: ${event.code})${c.reset}`);
|
|
@@ -3664,7 +3748,12 @@ class TunnelAgent {
|
|
|
3664
3748
|
}
|
|
3665
3749
|
if (msg.type === "auth_ok" && msg.signingKey) {
|
|
3666
3750
|
this.signingKey = msg.signingKey;
|
|
3667
|
-
|
|
3751
|
+
const capabilityNames = this.registry.getCapabilityNames();
|
|
3752
|
+
if (capabilityNames.length === 0) {
|
|
3753
|
+
log(`${c.yellow}!${c.reset}`, `Connected, but no capabilities are enabled — this tunnel cannot do anything. Run \`agent-tunnel connect --reauth\` to pair again.`);
|
|
3754
|
+
} else {
|
|
3755
|
+
log(`${c.green}●${c.reset}`, `Connected ${c.reset}${c.gray}(${capabilityNames.join(", ")})${c.reset}`);
|
|
3756
|
+
}
|
|
3668
3757
|
if (this.stableConnectionTimer)
|
|
3669
3758
|
clearTimeout(this.stableConnectionTimer);
|
|
3670
3759
|
this.stableConnectionTimer = setTimeout(() => {
|
|
@@ -3829,13 +3918,66 @@ class TunnelAgent {
|
|
|
3829
3918
|
}, delay);
|
|
3830
3919
|
}
|
|
3831
3920
|
buildWsUrl() {
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3921
|
+
return buildTunnelWsUrl(this.config);
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
// src/agent/banner.ts
|
|
3926
|
+
import { arch as arch2, hostname as hostname2, platform as platform2 } from "os";
|
|
3927
|
+
var BOX_WIDTH = 60;
|
|
3928
|
+
var BAR_WIDTH = 50;
|
|
3929
|
+
var BAR_FRAMES = 14;
|
|
3930
|
+
var WORDMARK = [
|
|
3931
|
+
`${c.cyan}▄▀█ █▀▀ █▀▀ █▄ █ ▀█▀${c.reset} ${c.cyan}▀█▀ █ █ █▄ █ █▄ █ █▀▀ █ ${c.reset}`,
|
|
3932
|
+
`${c.cyan}█▀█ █▄█ ██▄ █ ▀█ █${c.reset} ${c.cyan} █ █▄█ █ ▀█ █ ▀█ ██▄ █▄▄${c.reset}`
|
|
3933
|
+
];
|
|
3934
|
+
function truncate(value, max) {
|
|
3935
|
+
return value.length > max ? `${value.slice(0, max)}…` : value;
|
|
3936
|
+
}
|
|
3937
|
+
async function animateConnectingBar() {
|
|
3938
|
+
for (let frame = 0;frame <= BAR_FRAMES; frame++) {
|
|
3939
|
+
const filled = Math.round(frame / BAR_FRAMES * BAR_WIDTH);
|
|
3940
|
+
process.stdout.write(`\r ${c.cyan}◇${c.reset} ${c.cyan}${"═".repeat(filled)}${c.reset}${c.gray}${"─".repeat(BAR_WIDTH - filled)}${c.reset} `);
|
|
3941
|
+
await sleep(20);
|
|
3838
3942
|
}
|
|
3943
|
+
process.stdout.write(`\r ${c.cyan}◇ ${"═".repeat(BAR_WIDTH)} ◆${c.reset}
|
|
3944
|
+
`);
|
|
3945
|
+
await sleep(120);
|
|
3946
|
+
}
|
|
3947
|
+
async function printStartupBanner({
|
|
3948
|
+
tunnelId,
|
|
3949
|
+
apiUrl,
|
|
3950
|
+
capabilities,
|
|
3951
|
+
version
|
|
3952
|
+
}) {
|
|
3953
|
+
console.log("");
|
|
3954
|
+
for (const line of WORDMARK)
|
|
3955
|
+
console.log(` ${line}`);
|
|
3956
|
+
console.log("");
|
|
3957
|
+
await animateConnectingBar();
|
|
3958
|
+
const row = (content) => {
|
|
3959
|
+
const pad = Math.max(0, BOX_WIDTH - visibleLength(content));
|
|
3960
|
+
console.log(` ${c.gray}│${c.reset}${content}${" ".repeat(pad)}${c.gray}│${c.reset}`);
|
|
3961
|
+
};
|
|
3962
|
+
const blank = () => row("");
|
|
3963
|
+
const titleLeft = ` ${c.cyan}◆${c.reset} ${c.bold}${c.white}Agent Tunnel${c.reset}`;
|
|
3964
|
+
const titleRight = `${c.dim}v${version}${c.reset} `;
|
|
3965
|
+
const titlePad = Math.max(1, BOX_WIDTH - visibleLength(titleLeft) - visibleLength(titleRight));
|
|
3966
|
+
const capabilityRow = capabilities.length > 0 ? capabilities.map((name) => `${c.green}●${c.reset} ${c.white}${name}${c.reset}`).join(" ") : `${c.yellow}none — this tunnel cannot act${c.reset}`;
|
|
3967
|
+
const brand = "created by kortix";
|
|
3968
|
+
console.log("");
|
|
3969
|
+
console.log(` ${c.gray}╭${"─".repeat(BOX_WIDTH)}╮${c.reset}`);
|
|
3970
|
+
blank();
|
|
3971
|
+
row(`${titleLeft}${" ".repeat(titlePad)}${titleRight}`);
|
|
3972
|
+
row(` ${c.dim}Bridge between AI agents & local machines${c.reset}`);
|
|
3973
|
+
blank();
|
|
3974
|
+
row(` ${c.dim}tunnel${c.reset} ${c.white}${truncate(tunnelId, 40)}${c.reset}`);
|
|
3975
|
+
row(` ${c.dim}relay${c.reset} ${c.white}${truncate(apiUrl, 40)}${c.reset}`);
|
|
3976
|
+
row(` ${c.dim}machine${c.reset} ${c.white}${truncate(hostname2(), 28)}${c.reset} ${c.dim}(${platform2()} ${arch2()})${c.reset}`);
|
|
3977
|
+
row(` ${c.dim}access${c.reset} ${capabilityRow}`);
|
|
3978
|
+
blank();
|
|
3979
|
+
console.log(` ${c.gray}╰${"─".repeat(BOX_WIDTH - brand.length - 3)} ${c.dim}created by ${c.cyan}kortix${c.reset} ${c.gray}─╯${c.reset}`);
|
|
3980
|
+
console.log("");
|
|
3839
3981
|
}
|
|
3840
3982
|
|
|
3841
3983
|
// src/agent/capabilities/index.ts
|
|
@@ -3866,8 +4008,8 @@ class CapabilityRegistry {
|
|
|
3866
4008
|
// src/agent/capabilities/desktop/cua-driver.ts
|
|
3867
4009
|
import { spawn } from "child_process";
|
|
3868
4010
|
import { existsSync as existsSync2, realpathSync, statSync } from "fs";
|
|
3869
|
-
import { homedir as homedir2, platform as
|
|
3870
|
-
import { join as
|
|
4011
|
+
import { homedir as homedir2, platform as platform3 } from "os";
|
|
4012
|
+
import { join as join3 } from "path";
|
|
3871
4013
|
var MAX_DRIVER_OUTPUT_BYTES = 5 * 1024 * 1024;
|
|
3872
4014
|
var DRIVER_ENV_KEYS = [
|
|
3873
4015
|
"PATH",
|
|
@@ -3886,7 +4028,7 @@ var DRIVER_ENV_KEYS = [
|
|
|
3886
4028
|
function candidateBins() {
|
|
3887
4029
|
const candidates = [
|
|
3888
4030
|
process.env.CUA_DRIVER_BIN,
|
|
3889
|
-
|
|
4031
|
+
join3(homedir2(), ".local", "bin", process.platform === "win32" ? "cua-driver.exe" : "cua-driver"),
|
|
3890
4032
|
"/usr/local/bin/cua-driver",
|
|
3891
4033
|
"/opt/homebrew/bin/cua-driver"
|
|
3892
4034
|
];
|
|
@@ -3991,7 +4133,7 @@ function parseJsonOutput(stdout) {
|
|
|
3991
4133
|
function isDaemonProxyFallback(message) {
|
|
3992
4134
|
return message.includes("daemon proxy") && message.includes("Resource temporarily unavailable");
|
|
3993
4135
|
}
|
|
3994
|
-
async function
|
|
4136
|
+
async function sleep2(ms) {
|
|
3995
4137
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
3996
4138
|
}
|
|
3997
4139
|
function sanitizeArgs(args) {
|
|
@@ -4050,7 +4192,7 @@ class CuaDriver {
|
|
|
4050
4192
|
const { stdout, stderr } = await execFile(bin, ["call", tool, payload], 60000);
|
|
4051
4193
|
if (isDaemonProxyFallback(stderr)) {
|
|
4052
4194
|
lastError = new Error(stderr.trim());
|
|
4053
|
-
await
|
|
4195
|
+
await sleep2(150 * (attempt + 1));
|
|
4054
4196
|
continue;
|
|
4055
4197
|
}
|
|
4056
4198
|
return parseJsonOutput(stdout);
|
|
@@ -4060,14 +4202,14 @@ class CuaDriver {
|
|
|
4060
4202
|
throw err;
|
|
4061
4203
|
}
|
|
4062
4204
|
lastError = err;
|
|
4063
|
-
await
|
|
4205
|
+
await sleep2(150 * (attempt + 1));
|
|
4064
4206
|
}
|
|
4065
4207
|
}
|
|
4066
4208
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
4067
4209
|
}
|
|
4068
4210
|
async startDaemon() {
|
|
4069
4211
|
const bin = await this.ensureInstalled();
|
|
4070
|
-
if (
|
|
4212
|
+
if (platform3() === "darwin") {
|
|
4071
4213
|
const child = spawn("open", ["-n", "-g", "-a", "CuaDriver", "--args", "serve"], {
|
|
4072
4214
|
detached: true,
|
|
4073
4215
|
stdio: "ignore",
|
|
@@ -4222,10 +4364,10 @@ function createDesktopCapability() {
|
|
|
4222
4364
|
|
|
4223
4365
|
// src/agent/capabilities/filesystem.ts
|
|
4224
4366
|
import { open, writeFile, readdir, stat, unlink, mkdir } from "fs/promises";
|
|
4225
|
-
import { join as
|
|
4367
|
+
import { join as join5, dirname as dirname3 } from "path";
|
|
4226
4368
|
|
|
4227
4369
|
// src/agent/security/path-validator.ts
|
|
4228
|
-
import { dirname, basename, join as
|
|
4370
|
+
import { dirname as dirname2, basename, join as join4, resolve, normalize, relative, isAbsolute } from "path";
|
|
4229
4371
|
import { realpathSync as realpathSync2 } from "fs";
|
|
4230
4372
|
function resolveExistingRoot(path) {
|
|
4231
4373
|
const normalized = normalize(resolve(path));
|
|
@@ -4244,10 +4386,10 @@ function resolvePathForValidation(path) {
|
|
|
4244
4386
|
if (code !== "ENOENT") {
|
|
4245
4387
|
throw new Error(`Access denied: cannot resolve path "${path}" (${code})`);
|
|
4246
4388
|
}
|
|
4247
|
-
const parent =
|
|
4389
|
+
const parent = dirname2(normalized);
|
|
4248
4390
|
if (parent === normalized)
|
|
4249
4391
|
return normalized;
|
|
4250
|
-
return
|
|
4392
|
+
return join4(resolvePathForValidation(parent), basename(normalized));
|
|
4251
4393
|
}
|
|
4252
4394
|
}
|
|
4253
4395
|
function assertAllowedResolvedPath(originalPath, resolved, allowedPaths, blockedPaths = []) {
|
|
@@ -4281,8 +4423,8 @@ function validatePath(path, allowedPaths, blockedPaths = []) {
|
|
|
4281
4423
|
}
|
|
4282
4424
|
function validateWritePath(path, allowedPaths, blockedPaths = []) {
|
|
4283
4425
|
const resolved = validatePath(path, allowedPaths, blockedPaths);
|
|
4284
|
-
let parent =
|
|
4285
|
-
while (parent && parent !==
|
|
4426
|
+
let parent = dirname2(normalize(resolve(path)));
|
|
4427
|
+
while (parent && parent !== dirname2(parent)) {
|
|
4286
4428
|
try {
|
|
4287
4429
|
const resolvedParent = realpathSync2(parent);
|
|
4288
4430
|
assertAllowedResolvedPath(path, resolvedParent, allowedPaths, blockedPaths);
|
|
@@ -4292,7 +4434,7 @@ function validateWritePath(path, allowedPaths, blockedPaths = []) {
|
|
|
4292
4434
|
if (code !== "ENOENT") {
|
|
4293
4435
|
throw new Error(`Access denied: cannot resolve parent for "${path}" (${code})`);
|
|
4294
4436
|
}
|
|
4295
|
-
parent =
|
|
4437
|
+
parent = dirname2(parent);
|
|
4296
4438
|
}
|
|
4297
4439
|
}
|
|
4298
4440
|
throw new Error(`Access denied: cannot resolve parent for "${path}"`);
|
|
@@ -4387,7 +4529,7 @@ function createFilesystemCapability(config) {
|
|
|
4387
4529
|
if (contentBytes > maxFileSize) {
|
|
4388
4530
|
throw new Error(`Content exceeds max size (${contentBytes} > ${maxFileSize})`);
|
|
4389
4531
|
}
|
|
4390
|
-
await mkdir(
|
|
4532
|
+
await mkdir(dirname3(path), { recursive: true });
|
|
4391
4533
|
validateFilesystemPath(path, config, params, true);
|
|
4392
4534
|
await writeFile(path, content, { encoding });
|
|
4393
4535
|
validateFilesystemPath(path, config, params);
|
|
@@ -4405,7 +4547,7 @@ function createFilesystemCapability(config) {
|
|
|
4405
4547
|
const entries = await readdir(path, { withFileTypes: true });
|
|
4406
4548
|
const result = entries.map((entry) => ({
|
|
4407
4549
|
name: entry.name,
|
|
4408
|
-
path:
|
|
4550
|
+
path: join5(path, entry.name),
|
|
4409
4551
|
isDirectory: entry.isDirectory(),
|
|
4410
4552
|
isFile: entry.isFile(),
|
|
4411
4553
|
isSymlink: entry.isSymbolicLink()
|
|
@@ -4418,7 +4560,7 @@ function createFilesystemCapability(config) {
|
|
|
4418
4560
|
for (const sub of subEntries) {
|
|
4419
4561
|
result.push({
|
|
4420
4562
|
name: sub.name,
|
|
4421
|
-
path:
|
|
4563
|
+
path: join5(dir.path, sub.name),
|
|
4422
4564
|
isDirectory: sub.isDirectory(),
|
|
4423
4565
|
isFile: sub.isFile(),
|
|
4424
4566
|
isSymlink: sub.isSymbolicLink()
|
|
@@ -4597,771 +4739,868 @@ function createEnabledCapabilityRegistry(config, findDesktopDriver = findCuaDriv
|
|
|
4597
4739
|
return registry;
|
|
4598
4740
|
}
|
|
4599
4741
|
|
|
4600
|
-
// src/agent/
|
|
4601
|
-
import { existsSync as existsSync3, mkdirSync, readFileSync as
|
|
4602
|
-
import { homedir as homedir3
|
|
4603
|
-
import {
|
|
4604
|
-
|
|
4605
|
-
var
|
|
4606
|
-
var
|
|
4607
|
-
function
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
systemdUnit: join5(home, ".config", "systemd", "user", `${SERVICE_LABEL}.service`),
|
|
4615
|
-
windowsScript: join5(configDir, "agent-tunnel-service.ps1")
|
|
4616
|
-
};
|
|
4617
|
-
}
|
|
4618
|
-
function shellQuote(value) {
|
|
4619
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
4742
|
+
// src/agent/credential-store.ts
|
|
4743
|
+
import { chmodSync as chmodSync2, existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, renameSync, rmSync, writeFileSync } from "fs";
|
|
4744
|
+
import { homedir as homedir3 } from "os";
|
|
4745
|
+
import { join as join6 } from "path";
|
|
4746
|
+
var CONFIG_DIR2 = join6(homedir3(), ".agent-tunnel");
|
|
4747
|
+
var CONFIG_FILE2 = join6(CONFIG_DIR2, "config.json");
|
|
4748
|
+
var PAIRING_FIELDS = ["token", "tunnelId", "enabledCapabilities"];
|
|
4749
|
+
function readConfigFile() {
|
|
4750
|
+
try {
|
|
4751
|
+
const parsed = JSON.parse(readFileSync3(CONFIG_FILE2, "utf-8"));
|
|
4752
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
4753
|
+
} catch {
|
|
4754
|
+
return {};
|
|
4755
|
+
}
|
|
4620
4756
|
}
|
|
4621
|
-
function
|
|
4622
|
-
|
|
4757
|
+
function writeConfigFileAtomic(next) {
|
|
4758
|
+
mkdirSync(CONFIG_DIR2, { recursive: true, mode: 448 });
|
|
4759
|
+
try {
|
|
4760
|
+
chmodSync2(CONFIG_DIR2, 448);
|
|
4761
|
+
} catch {}
|
|
4762
|
+
const tmpFile = join6(CONFIG_DIR2, `config.${process.pid}.${Date.now()}.tmp`);
|
|
4763
|
+
writeFileSync(tmpFile, JSON.stringify(next, null, 2), { mode: 384, flag: "wx" });
|
|
4764
|
+
try {
|
|
4765
|
+
chmodSync2(tmpFile, 384);
|
|
4766
|
+
} catch {}
|
|
4767
|
+
renameSync(tmpFile, CONFIG_FILE2);
|
|
4768
|
+
try {
|
|
4769
|
+
chmodSync2(CONFIG_FILE2, 384);
|
|
4770
|
+
} catch {}
|
|
4623
4771
|
}
|
|
4624
|
-
function
|
|
4625
|
-
|
|
4772
|
+
function saveCredentials(tunnelId, token, apiUrl, enabledCapabilities) {
|
|
4773
|
+
writeConfigFileAtomic({
|
|
4774
|
+
...readConfigFile(),
|
|
4775
|
+
tunnelId,
|
|
4776
|
+
token,
|
|
4777
|
+
apiUrl,
|
|
4778
|
+
...enabledCapabilities !== undefined ? { enabledCapabilities } : {}
|
|
4779
|
+
});
|
|
4626
4780
|
}
|
|
4627
|
-
function
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4781
|
+
function clearSavedCredentials() {
|
|
4782
|
+
if (!existsSync3(CONFIG_FILE2))
|
|
4783
|
+
return false;
|
|
4784
|
+
let existing;
|
|
4785
|
+
try {
|
|
4786
|
+
existing = JSON.parse(readFileSync3(CONFIG_FILE2, "utf-8"));
|
|
4787
|
+
} catch {
|
|
4788
|
+
rmSync(CONFIG_FILE2, { force: true });
|
|
4789
|
+
return true;
|
|
4632
4790
|
}
|
|
4633
|
-
|
|
4791
|
+
const hadCredentials = PAIRING_FIELDS.some((key) => (key in existing) && existing[key] != null);
|
|
4792
|
+
for (const key of PAIRING_FIELDS)
|
|
4793
|
+
delete existing[key];
|
|
4794
|
+
writeConfigFileAtomic(existing);
|
|
4795
|
+
return hadCredentials;
|
|
4634
4796
|
}
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4797
|
+
|
|
4798
|
+
// src/agent/credential-probe.ts
|
|
4799
|
+
var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
|
|
4800
|
+
function probeCredentials(config, options = {}) {
|
|
4801
|
+
return new Promise((resolve2) => {
|
|
4802
|
+
let socket;
|
|
4803
|
+
try {
|
|
4804
|
+
socket = new WebSocket(new URL(buildTunnelWsUrl(config)));
|
|
4805
|
+
} catch {
|
|
4806
|
+
resolve2("unreachable");
|
|
4807
|
+
return;
|
|
4808
|
+
}
|
|
4809
|
+
const settle = (result) => {
|
|
4810
|
+
clearTimeout(timer);
|
|
4811
|
+
try {
|
|
4812
|
+
socket.close(1000, "probe complete");
|
|
4813
|
+
} catch {}
|
|
4814
|
+
resolve2(result);
|
|
4815
|
+
};
|
|
4816
|
+
const timer = setTimeout(() => settle("unreachable"), options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS);
|
|
4817
|
+
socket.addEventListener("open", () => {
|
|
4818
|
+
try {
|
|
4819
|
+
socket.send(JSON.stringify({
|
|
4820
|
+
type: "auth",
|
|
4821
|
+
token: trustedCredential(config.token, "token"),
|
|
4822
|
+
capabilities: options.capabilities ?? [],
|
|
4823
|
+
agentVersion: AGENT_VERSION
|
|
4824
|
+
}));
|
|
4825
|
+
} catch {
|
|
4826
|
+
settle("unreachable");
|
|
4827
|
+
}
|
|
4828
|
+
});
|
|
4829
|
+
socket.addEventListener("message", (event) => {
|
|
4830
|
+
try {
|
|
4831
|
+
const message = JSON.parse(String(event.data));
|
|
4832
|
+
if (isJsonRecord(message) && message.type === "auth_ok")
|
|
4833
|
+
settle("valid");
|
|
4834
|
+
} catch {}
|
|
4835
|
+
});
|
|
4836
|
+
socket.addEventListener("close", (event) => {
|
|
4837
|
+
const { code } = event;
|
|
4838
|
+
if (AUTH_REJECTED_CLOSE_CODES.includes(code))
|
|
4839
|
+
return settle("rejected");
|
|
4840
|
+
if (code === AGENT_REPLACED_CLOSE_CODE)
|
|
4841
|
+
return settle("valid");
|
|
4842
|
+
settle("unreachable");
|
|
4843
|
+
});
|
|
4844
|
+
socket.addEventListener("error", () => settle("unreachable"));
|
|
4845
|
+
});
|
|
4638
4846
|
}
|
|
4639
|
-
function
|
|
4640
|
-
return
|
|
4847
|
+
function isJsonRecord(value) {
|
|
4848
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4641
4849
|
}
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4850
|
+
|
|
4851
|
+
// src/agent/device-auth.ts
|
|
4852
|
+
import { spawn as spawn3 } from "child_process";
|
|
4853
|
+
import { hostname as hostname3, platform as platform4 } from "os";
|
|
4854
|
+
var TUNNEL_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
4855
|
+
var SETUP_TOKEN_PATTERN = /^kortix_tnl_[A-Za-z0-9_-]{32,64}$/;
|
|
4856
|
+
var DEVICE_CODE_PATTERN = /^[A-Z]{4}-[0-9]{4}$/;
|
|
4857
|
+
var DEVICE_SECRET_PATTERN = /^[A-Za-z0-9]{32}$/;
|
|
4858
|
+
var MAX_CHALLENGE_LIFETIME_MS = 10 * 60000;
|
|
4859
|
+
|
|
4860
|
+
class InvalidDeviceAuthResponseError extends Error {
|
|
4861
|
+
constructor(message) {
|
|
4862
|
+
super(message);
|
|
4863
|
+
this.name = "InvalidDeviceAuthResponseError";
|
|
4864
|
+
}
|
|
4649
4865
|
}
|
|
4650
|
-
|
|
4866
|
+
function invalid(what) {
|
|
4867
|
+
throw new InvalidDeviceAuthResponseError(`Authorization server returned an invalid ${what}`);
|
|
4651
4868
|
}
|
|
4652
|
-
function
|
|
4653
|
-
return
|
|
4869
|
+
function isJsonRecord2(value) {
|
|
4870
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4654
4871
|
}
|
|
4655
|
-
function
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
<key>Label</key>
|
|
4663
|
-
<string>${xmlEscape(SERVICE_LABEL)}</string>
|
|
4664
|
-
<key>ProgramArguments</key>
|
|
4665
|
-
<array>
|
|
4666
|
-
<string>/bin/sh</string>
|
|
4667
|
-
<string>-lc</string>
|
|
4668
|
-
<string>${xmlEscape(command)}</string>
|
|
4669
|
-
</array>
|
|
4670
|
-
<key>RunAtLoad</key>
|
|
4671
|
-
<true/>
|
|
4672
|
-
<key>KeepAlive</key>
|
|
4673
|
-
<true/>
|
|
4674
|
-
<key>Umask</key>
|
|
4675
|
-
<integer>63</integer>
|
|
4676
|
-
<key>StandardOutPath</key>
|
|
4677
|
-
<string>${xmlEscape(stdout)}</string>
|
|
4678
|
-
<key>StandardErrorPath</key>
|
|
4679
|
-
<string>${xmlEscape(stderr)}</string>
|
|
4680
|
-
<key>WorkingDirectory</key>
|
|
4681
|
-
<string>${xmlEscape(homedir3())}</string>
|
|
4682
|
-
<key>EnvironmentVariables</key>
|
|
4683
|
-
<dict>
|
|
4684
|
-
<key>PATH</key>
|
|
4685
|
-
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
4686
|
-
</dict>
|
|
4687
|
-
</dict>
|
|
4688
|
-
</plist>
|
|
4689
|
-
`;
|
|
4872
|
+
function normalizeBrowserUrl(value) {
|
|
4873
|
+
try {
|
|
4874
|
+
const url = new URL(value);
|
|
4875
|
+
return url.protocol === "https:" || url.protocol === "http:" ? url.toString() : null;
|
|
4876
|
+
} catch {
|
|
4877
|
+
return null;
|
|
4878
|
+
}
|
|
4690
4879
|
}
|
|
4691
|
-
function
|
|
4692
|
-
|
|
4693
|
-
const stderr = join5(paths.logDir, "agent-tunnel.err.log");
|
|
4694
|
-
return `[Unit]
|
|
4695
|
-
Description=Kortix Agent Tunnel
|
|
4696
|
-
After=network-online.target
|
|
4697
|
-
Wants=network-online.target
|
|
4698
|
-
|
|
4699
|
-
[Service]
|
|
4700
|
-
Type=simple
|
|
4701
|
-
UMask=0077
|
|
4702
|
-
ExecStart=/bin/sh -lc ${shellQuote(command)}
|
|
4703
|
-
Restart=always
|
|
4704
|
-
RestartSec=5
|
|
4705
|
-
WorkingDirectory=${homedir3()}
|
|
4706
|
-
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
|
4707
|
-
StandardOutput=append:${stdout}
|
|
4708
|
-
StandardError=append:${stderr}
|
|
4709
|
-
|
|
4710
|
-
[Install]
|
|
4711
|
-
WantedBy=default.target
|
|
4712
|
-
`;
|
|
4880
|
+
function isLoopback(url) {
|
|
4881
|
+
return ["localhost", "127.0.0.1", "[::1]", "::1"].includes(url.hostname);
|
|
4713
4882
|
}
|
|
4714
|
-
function
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4883
|
+
function assertSafeVerificationUrl(value) {
|
|
4884
|
+
if (value.length > 2048)
|
|
4885
|
+
invalid("verification URL");
|
|
4886
|
+
const browserUrl = normalizeBrowserUrl(value);
|
|
4887
|
+
if (!browserUrl)
|
|
4888
|
+
invalid("verification URL");
|
|
4889
|
+
const url = new URL(browserUrl);
|
|
4890
|
+
if (url.username || url.password || url.protocol !== "https:" && !isLoopback(url)) {
|
|
4891
|
+
throw new InvalidDeviceAuthResponseError("Authorization server returned an unsafe verification URL");
|
|
4892
|
+
}
|
|
4893
|
+
return browserUrl;
|
|
4719
4894
|
}
|
|
4720
|
-
function
|
|
4721
|
-
|
|
4722
|
-
|
|
4895
|
+
function parseDeviceAuthChallenge(value) {
|
|
4896
|
+
if (!isJsonRecord2(value))
|
|
4897
|
+
invalid("challenge");
|
|
4898
|
+
const { deviceCode, deviceSecret, verificationUrl, expiresAt, pollIntervalMs } = value;
|
|
4899
|
+
if (typeof deviceCode !== "string" || !DEVICE_CODE_PATTERN.test(deviceCode))
|
|
4900
|
+
invalid("device code");
|
|
4901
|
+
if (typeof deviceSecret !== "string" || !DEVICE_SECRET_PATTERN.test(deviceSecret)) {
|
|
4902
|
+
invalid("device secret");
|
|
4903
|
+
}
|
|
4904
|
+
if (typeof verificationUrl !== "string")
|
|
4905
|
+
invalid("verification URL");
|
|
4906
|
+
if (typeof expiresAt !== "string")
|
|
4907
|
+
invalid("expiration");
|
|
4908
|
+
const expiresAtMs = Date.parse(expiresAt);
|
|
4909
|
+
const now = Date.now();
|
|
4910
|
+
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= now || expiresAtMs > now + MAX_CHALLENGE_LIFETIME_MS) {
|
|
4911
|
+
invalid("expiration");
|
|
4912
|
+
}
|
|
4913
|
+
if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 250 || pollIntervalMs > 1e4) {
|
|
4914
|
+
invalid("poll interval");
|
|
4915
|
+
}
|
|
4916
|
+
return {
|
|
4917
|
+
deviceCode,
|
|
4918
|
+
deviceSecret,
|
|
4919
|
+
verificationUrl: assertSafeVerificationUrl(verificationUrl),
|
|
4920
|
+
expiresAt,
|
|
4921
|
+
pollIntervalMs
|
|
4922
|
+
};
|
|
4723
4923
|
}
|
|
4724
|
-
function
|
|
4725
|
-
const
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4924
|
+
function parseApprovedCredentials(value) {
|
|
4925
|
+
const { tunnelId, token } = value;
|
|
4926
|
+
if (typeof tunnelId !== "string" || !TUNNEL_ID_PATTERN.test(tunnelId))
|
|
4927
|
+
invalid("tunnel ID");
|
|
4928
|
+
if (typeof token !== "string" || !SETUP_TOKEN_PATTERN.test(token))
|
|
4929
|
+
invalid("setup token");
|
|
4930
|
+
return { tunnelId, token };
|
|
4931
|
+
}
|
|
4932
|
+
function parseApprovedCapabilities(value) {
|
|
4933
|
+
if (!Array.isArray(value))
|
|
4934
|
+
return [];
|
|
4935
|
+
return [...new Set(value)].filter((item) => isTunnelCapability(item));
|
|
4936
|
+
}
|
|
4937
|
+
function parseDeviceAuthStatus(value) {
|
|
4938
|
+
if (!isJsonRecord2(value) || typeof value.status !== "string")
|
|
4939
|
+
invalid("status response");
|
|
4940
|
+
switch (value.status) {
|
|
4941
|
+
case "approved": {
|
|
4942
|
+
if (!value.tunnelId || !value.token)
|
|
4943
|
+
return { status: "approved-without-token" };
|
|
4944
|
+
return {
|
|
4945
|
+
status: "approved",
|
|
4946
|
+
...parseApprovedCredentials(value),
|
|
4947
|
+
capabilities: parseApprovedCapabilities(value.capabilities)
|
|
4948
|
+
};
|
|
4949
|
+
}
|
|
4950
|
+
case "denied":
|
|
4951
|
+
return { status: "denied" };
|
|
4952
|
+
case "expired":
|
|
4953
|
+
return { status: "expired" };
|
|
4954
|
+
default:
|
|
4955
|
+
return null;
|
|
4743
4956
|
}
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
detail: [reload.detail, enable.detail].filter(Boolean).join(`
|
|
4755
|
-
`)
|
|
4756
|
-
};
|
|
4957
|
+
}
|
|
4958
|
+
async function requestDeviceAuthorization(apiUrl) {
|
|
4959
|
+
const response = await fetch(`${apiUrl}/device-auth`, {
|
|
4960
|
+
method: "POST",
|
|
4961
|
+
headers: { "Content-Type": "application/json" },
|
|
4962
|
+
body: JSON.stringify({ machineHostname: hostname3() })
|
|
4963
|
+
});
|
|
4964
|
+
if (!response.ok) {
|
|
4965
|
+
const body = await response.text().catch(() => "");
|
|
4966
|
+
throw new InvalidDeviceAuthResponseError(`Failed to create device auth request: ${response.status} ${body.slice(0, 200)}`);
|
|
4757
4967
|
}
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
"
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4968
|
+
return parseDeviceAuthChallenge(await response.json());
|
|
4969
|
+
}
|
|
4970
|
+
async function awaitDeviceAuthorization(apiUrl, challenge, options = {}) {
|
|
4971
|
+
const wait = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
4972
|
+
const expiresAtMs = Date.parse(challenge.expiresAt);
|
|
4973
|
+
for (;; ) {
|
|
4974
|
+
const secondsRemaining = Math.max(0, Math.floor((expiresAtMs - Date.now()) / 1000));
|
|
4975
|
+
if (secondsRemaining <= 0)
|
|
4976
|
+
return { status: "expired" };
|
|
4977
|
+
options.onWaiting?.(secondsRemaining);
|
|
4978
|
+
let payload;
|
|
4979
|
+
try {
|
|
4980
|
+
const response = await fetch(`${apiUrl}/device-auth/${challenge.deviceCode}/status`, {
|
|
4981
|
+
headers: { Authorization: `Bearer ${challenge.deviceSecret}` }
|
|
4982
|
+
});
|
|
4983
|
+
if (!response.ok) {
|
|
4984
|
+
await wait(challenge.pollIntervalMs);
|
|
4985
|
+
continue;
|
|
4986
|
+
}
|
|
4987
|
+
payload = await response.json();
|
|
4988
|
+
} catch {
|
|
4989
|
+
await wait(challenge.pollIntervalMs);
|
|
4990
|
+
continue;
|
|
4991
|
+
}
|
|
4992
|
+
const outcome = parseDeviceAuthStatus(payload);
|
|
4993
|
+
if (outcome)
|
|
4994
|
+
return outcome;
|
|
4995
|
+
await wait(challenge.pollIntervalMs);
|
|
4781
4996
|
}
|
|
4782
|
-
throw new Error("Background service install is currently supported on macOS launchd, Linux systemd user services, and Windows Scheduled Tasks.");
|
|
4783
4997
|
}
|
|
4784
|
-
function
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4998
|
+
function openBrowser(url) {
|
|
4999
|
+
if (process.env.KORTIX_AGENT_TUNNEL_NO_BROWSER === "1")
|
|
5000
|
+
return;
|
|
5001
|
+
const safeUrl = normalizeBrowserUrl(url);
|
|
5002
|
+
if (!safeUrl)
|
|
5003
|
+
return;
|
|
5004
|
+
const opener = {
|
|
5005
|
+
darwin: ["open", [safeUrl]],
|
|
5006
|
+
win32: ["rundll32.exe", ["url.dll,FileProtocolHandler", safeUrl]]
|
|
5007
|
+
};
|
|
5008
|
+
const [command, args] = opener[platform4()] ?? ["xdg-open", [safeUrl]];
|
|
5009
|
+
try {
|
|
5010
|
+
spawn3(command, args, { detached: true, stdio: "ignore" }).unref();
|
|
5011
|
+
} catch {}
|
|
5012
|
+
}
|
|
5013
|
+
|
|
5014
|
+
// src/agent/log-format.ts
|
|
5015
|
+
function collapseRepeatedLines(lines) {
|
|
5016
|
+
const collapsed = [];
|
|
5017
|
+
let index = 0;
|
|
5018
|
+
while (index < lines.length) {
|
|
5019
|
+
let repeats = 1;
|
|
5020
|
+
while (index + repeats < lines.length && lines[index + repeats] === lines[index])
|
|
5021
|
+
repeats++;
|
|
5022
|
+
collapsed.push(repeats > 1 ? `${lines[index]} (x${repeats})` : lines[index]);
|
|
5023
|
+
index += repeats;
|
|
5024
|
+
}
|
|
5025
|
+
return collapsed;
|
|
5026
|
+
}
|
|
5027
|
+
function isShellStartupNoise(line) {
|
|
5028
|
+
return /\/\.(profile|bash_profile|zprofile|zshrc|bashrc)\b.*:.*(No such file or directory|command not found)/.test(line);
|
|
5029
|
+
}
|
|
5030
|
+
|
|
5031
|
+
// src/agent/prompts.ts
|
|
5032
|
+
import { createInterface } from "readline/promises";
|
|
5033
|
+
function isInteractiveTerminal() {
|
|
5034
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
5035
|
+
}
|
|
5036
|
+
function isTruthyFlag(value) {
|
|
5037
|
+
return value === "true" || value === "1" || value === "yes";
|
|
5038
|
+
}
|
|
5039
|
+
function anyFlag(flags, names) {
|
|
5040
|
+
return names.some((name) => isTruthyFlag(flags[name]));
|
|
5041
|
+
}
|
|
5042
|
+
async function promptYesNo(question, defaultValue) {
|
|
5043
|
+
const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
|
|
5044
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
5045
|
+
try {
|
|
5046
|
+
for (;; ) {
|
|
5047
|
+
const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase();
|
|
5048
|
+
if (!answer)
|
|
5049
|
+
return defaultValue;
|
|
5050
|
+
if (["y", "yes"].includes(answer))
|
|
5051
|
+
return true;
|
|
5052
|
+
if (["n", "no"].includes(answer))
|
|
5053
|
+
return false;
|
|
5054
|
+
console.log(` ${c.yellow}!${c.reset} Please answer yes or no.`);
|
|
5055
|
+
}
|
|
5056
|
+
} catch (error) {
|
|
5057
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
5058
|
+
process.stdout.write(`
|
|
5059
|
+
`);
|
|
5060
|
+
process.exit(130);
|
|
5061
|
+
}
|
|
5062
|
+
throw error;
|
|
5063
|
+
} finally {
|
|
5064
|
+
rl.close();
|
|
5065
|
+
}
|
|
5066
|
+
}
|
|
5067
|
+
|
|
5068
|
+
// src/agent/service.ts
|
|
5069
|
+
import { chmodSync as chmodSync3, copyFileSync, existsSync as existsSync5, mkdirSync as mkdirSync3, realpathSync as realpathSync3, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
5070
|
+
import { platform as platform6 } from "os";
|
|
5071
|
+
import { join as join9 } from "path";
|
|
5072
|
+
|
|
5073
|
+
// src/agent/service-drivers.ts
|
|
5074
|
+
import { spawnSync } from "child_process";
|
|
5075
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
5076
|
+
import { homedir as homedir5, platform as platform5, userInfo } from "os";
|
|
5077
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
5078
|
+
|
|
5079
|
+
// src/agent/service-quoting.ts
|
|
5080
|
+
function shellQuote(value) {
|
|
5081
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
5082
|
+
}
|
|
5083
|
+
function powershellQuote(value) {
|
|
5084
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
5085
|
+
}
|
|
5086
|
+
function xmlEscape(value) {
|
|
5087
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
5088
|
+
}
|
|
5089
|
+
|
|
5090
|
+
// src/agent/service-paths.ts
|
|
5091
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
5092
|
+
import { homedir as homedir4 } from "os";
|
|
5093
|
+
import { join as join7 } from "path";
|
|
5094
|
+
var SERVICE_LABEL = "ai.kortix.agent-tunnel";
|
|
5095
|
+
var DEFAULT_INSTALL_BACKGROUND_SERVICE = true;
|
|
5096
|
+
var TERMINAL_SERVICE_EXIT_CODE = 0;
|
|
5097
|
+
var MAX_SERVICE_LOG_BYTES = 5 * 1024 * 1024;
|
|
5098
|
+
var RETAINED_LOG_LINES = 500;
|
|
5099
|
+
function getServicePaths() {
|
|
5100
|
+
const home = homedir4();
|
|
5101
|
+
const configDir = join7(home, ".agent-tunnel");
|
|
5102
|
+
const binDir = join7(configDir, "bin");
|
|
5103
|
+
return {
|
|
5104
|
+
configDir,
|
|
5105
|
+
logDir: join7(configDir, "logs"),
|
|
5106
|
+
binDir,
|
|
5107
|
+
vendoredRunner: join7(binDir, "agent-cli.js"),
|
|
5108
|
+
launchdPlist: join7(home, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`),
|
|
5109
|
+
systemdUnit: join7(home, ".config", "systemd", "user", `${SERVICE_LABEL}.service`),
|
|
5110
|
+
windowsScript: join7(configDir, "agent-tunnel-service.ps1")
|
|
5111
|
+
};
|
|
5112
|
+
}
|
|
5113
|
+
function serviceLogFiles(paths = getServicePaths()) {
|
|
5114
|
+
return [
|
|
5115
|
+
join7(paths.logDir, "agent-tunnel.out.log"),
|
|
5116
|
+
join7(paths.logDir, "agent-tunnel.err.log")
|
|
5117
|
+
];
|
|
5118
|
+
}
|
|
5119
|
+
function rotateServiceLogs(paths = getServicePaths(), maxBytes = MAX_SERVICE_LOG_BYTES) {
|
|
5120
|
+
const rotated = [];
|
|
5121
|
+
for (const file of serviceLogFiles(paths)) {
|
|
5122
|
+
try {
|
|
5123
|
+
const contents = readFileSync4(file, "utf8");
|
|
5124
|
+
if (Buffer.byteLength(contents, "utf8") <= maxBytes)
|
|
5125
|
+
continue;
|
|
5126
|
+
const kept = contents.split(/\r?\n/).slice(-RETAINED_LOG_LINES).join(`
|
|
5127
|
+
`);
|
|
5128
|
+
writeFileSync2(file, `[agent-tunnel] earlier entries trimmed
|
|
5129
|
+
${kept}`, { mode: 384 });
|
|
5130
|
+
rotated.push(file);
|
|
5131
|
+
} catch {}
|
|
5132
|
+
}
|
|
5133
|
+
return rotated;
|
|
5134
|
+
}
|
|
5135
|
+
|
|
5136
|
+
// src/agent/service-drivers.ts
|
|
5137
|
+
function posixShellCommand(runner) {
|
|
5138
|
+
const interpreter = `"$(command -v ${shellQuote(runner.command)} 2>/dev/null || command -v node)"`;
|
|
5139
|
+
return `exec ${interpreter} ${runner.args.map(shellQuote).join(" ")}`;
|
|
5140
|
+
}
|
|
5141
|
+
function run(command, args) {
|
|
5142
|
+
const result = spawnSync(command, args, { encoding: "utf8" });
|
|
5143
|
+
return {
|
|
5144
|
+
ok: result.status === 0,
|
|
5145
|
+
detail: [result.stdout, result.stderr].filter(Boolean).join(`
|
|
5146
|
+
`).trim()
|
|
5147
|
+
};
|
|
5148
|
+
}
|
|
5149
|
+
var notInstalled = (what) => ({ ok: false, detail: `${what} is not installed.` });
|
|
5150
|
+
function joinDetails(...results) {
|
|
5151
|
+
return results.map((result) => result.detail).filter(Boolean).join(`
|
|
5152
|
+
`);
|
|
5153
|
+
}
|
|
5154
|
+
function launchdTarget() {
|
|
5155
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : userInfo().uid;
|
|
5156
|
+
return `gui/${uid}`;
|
|
5157
|
+
}
|
|
5158
|
+
function logPaths(paths) {
|
|
5159
|
+
return {
|
|
5160
|
+
stdout: join8(paths.logDir, "agent-tunnel.out.log"),
|
|
5161
|
+
stderr: join8(paths.logDir, "agent-tunnel.err.log")
|
|
5162
|
+
};
|
|
5163
|
+
}
|
|
5164
|
+
function renderLaunchdPlist(command, paths = getServicePaths()) {
|
|
5165
|
+
const { stdout, stderr } = logPaths(paths);
|
|
5166
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
5167
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
5168
|
+
<plist version="1.0">
|
|
5169
|
+
<dict>
|
|
5170
|
+
<key>Label</key>
|
|
5171
|
+
<string>${xmlEscape(SERVICE_LABEL)}</string>
|
|
5172
|
+
<key>ProgramArguments</key>
|
|
5173
|
+
<array>
|
|
5174
|
+
<string>/bin/sh</string>
|
|
5175
|
+
<string>-lc</string>
|
|
5176
|
+
<string>${xmlEscape(command)}</string>
|
|
5177
|
+
</array>
|
|
5178
|
+
<key>RunAtLoad</key>
|
|
5179
|
+
<true/>
|
|
5180
|
+
<key>KeepAlive</key>
|
|
5181
|
+
<dict>
|
|
5182
|
+
<key>SuccessfulExit</key>
|
|
5183
|
+
<false/>
|
|
5184
|
+
</dict>
|
|
5185
|
+
<key>Umask</key>
|
|
5186
|
+
<integer>63</integer>
|
|
5187
|
+
<key>StandardOutPath</key>
|
|
5188
|
+
<string>${xmlEscape(stdout)}</string>
|
|
5189
|
+
<key>StandardErrorPath</key>
|
|
5190
|
+
<string>${xmlEscape(stderr)}</string>
|
|
5191
|
+
<key>WorkingDirectory</key>
|
|
5192
|
+
<string>${xmlEscape(homedir5())}</string>
|
|
5193
|
+
<key>EnvironmentVariables</key>
|
|
5194
|
+
<dict>
|
|
5195
|
+
<key>PATH</key>
|
|
5196
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
5197
|
+
</dict>
|
|
5198
|
+
</dict>
|
|
5199
|
+
</plist>
|
|
5200
|
+
`;
|
|
5201
|
+
}
|
|
5202
|
+
function renderSystemdUnit(command, paths = getServicePaths()) {
|
|
5203
|
+
const { stdout, stderr } = logPaths(paths);
|
|
5204
|
+
return `[Unit]
|
|
5205
|
+
Description=Kortix Agent Tunnel
|
|
5206
|
+
After=network-online.target
|
|
5207
|
+
Wants=network-online.target
|
|
5208
|
+
|
|
5209
|
+
[Service]
|
|
5210
|
+
Type=simple
|
|
5211
|
+
UMask=0077
|
|
5212
|
+
ExecStart=/bin/sh -lc ${shellQuote(command)}
|
|
5213
|
+
Restart=on-failure
|
|
5214
|
+
RestartSec=5
|
|
5215
|
+
WorkingDirectory=${homedir5()}
|
|
5216
|
+
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
|
5217
|
+
StandardOutput=append:${stdout}
|
|
5218
|
+
StandardError=append:${stderr}
|
|
5219
|
+
|
|
5220
|
+
[Install]
|
|
5221
|
+
WantedBy=default.target
|
|
5222
|
+
`;
|
|
5223
|
+
}
|
|
5224
|
+
function renderWindowsPowerShellScript(runner) {
|
|
5225
|
+
const command = powershellQuote(runner.command);
|
|
5226
|
+
const args = runner.args.map(powershellQuote).join(" ");
|
|
5227
|
+
return `$ErrorActionPreference = 'Continue'
|
|
5228
|
+
while ($true) {
|
|
5229
|
+
& ${command}${args ? ` ${args}` : ""}
|
|
5230
|
+
# A clean exit means the agent stopped for a reason restarting cannot fix,
|
|
5231
|
+
# such as a missing or revoked credential. Anything else is a crash worth retrying.
|
|
5232
|
+
if ($LASTEXITCODE -eq ${TERMINAL_SERVICE_EXIT_CODE}) { break }
|
|
5233
|
+
Start-Sleep -Seconds 5
|
|
5234
|
+
}
|
|
5235
|
+
`;
|
|
5236
|
+
}
|
|
5237
|
+
var launchd = {
|
|
5238
|
+
unitPath: (paths) => paths.launchdPlist,
|
|
5239
|
+
install(paths, runner) {
|
|
5240
|
+
mkdirSync2(dirname4(paths.launchdPlist), { recursive: true });
|
|
5241
|
+
writeFileSync3(paths.launchdPlist, renderLaunchdPlist(posixShellCommand(runner), paths), { mode: 384 });
|
|
5242
|
+
run("launchctl", ["bootout", launchdTarget(), paths.launchdPlist]);
|
|
5243
|
+
const boot = run("launchctl", ["bootstrap", launchdTarget(), paths.launchdPlist]);
|
|
5244
|
+
const kick = run("launchctl", ["kickstart", "-k", `${launchdTarget()}/${SERVICE_LABEL}`]);
|
|
5245
|
+
return { installed: true, active: boot.ok || kick.ok ? true : null, detail: joinDetails(boot, kick) };
|
|
5246
|
+
},
|
|
5247
|
+
uninstall(paths) {
|
|
5248
|
+
const existed = existsSync4(paths.launchdPlist);
|
|
4788
5249
|
const stop = run("launchctl", ["bootout", launchdTarget(), paths.launchdPlist]);
|
|
4789
5250
|
if (existed)
|
|
4790
|
-
|
|
5251
|
+
rmSync2(paths.launchdPlist, { force: true });
|
|
5252
|
+
return { detail: stop.detail };
|
|
5253
|
+
},
|
|
5254
|
+
start(paths, installed) {
|
|
5255
|
+
const boot = installed ? run("launchctl", ["bootstrap", launchdTarget(), paths.launchdPlist]) : notInstalled("LaunchAgent");
|
|
5256
|
+
const kick = run("launchctl", ["kickstart", "-k", `${launchdTarget()}/${SERVICE_LABEL}`]);
|
|
5257
|
+
return { active: boot.ok || kick.ok ? true : null, detail: joinDetails(boot, kick) };
|
|
5258
|
+
},
|
|
5259
|
+
stop(paths, installed) {
|
|
5260
|
+
const stop = installed ? run("launchctl", ["bootout", launchdTarget(), paths.launchdPlist]) : notInstalled("LaunchAgent");
|
|
5261
|
+
return { detail: stop.detail };
|
|
5262
|
+
},
|
|
5263
|
+
status(paths, installed) {
|
|
5264
|
+
const status = run("launchctl", ["print", `${launchdTarget()}/${SERVICE_LABEL}`]);
|
|
4791
5265
|
return {
|
|
4792
|
-
|
|
4793
|
-
installed:
|
|
4794
|
-
active: false,
|
|
4795
|
-
path: paths.launchdPlist,
|
|
4796
|
-
detail: stop.detail
|
|
5266
|
+
active: status.ok,
|
|
5267
|
+
detail: status.detail || (installed ? readFileSync5(paths.launchdPlist, "utf8") : undefined)
|
|
4797
5268
|
};
|
|
4798
5269
|
}
|
|
4799
|
-
|
|
4800
|
-
|
|
5270
|
+
};
|
|
5271
|
+
var systemd = {
|
|
5272
|
+
unitPath: (paths) => paths.systemdUnit,
|
|
5273
|
+
install(paths, runner) {
|
|
5274
|
+
mkdirSync2(dirname4(paths.systemdUnit), { recursive: true });
|
|
5275
|
+
writeFileSync3(paths.systemdUnit, renderSystemdUnit(posixShellCommand(runner), paths), { mode: 384 });
|
|
5276
|
+
const reload = run("systemctl", ["--user", "daemon-reload"]);
|
|
5277
|
+
const enable = run("systemctl", ["--user", "enable", "--now", `${SERVICE_LABEL}.service`]);
|
|
5278
|
+
return { installed: true, active: enable.ok ? true : null, detail: joinDetails(reload, enable) };
|
|
5279
|
+
},
|
|
5280
|
+
uninstall(paths) {
|
|
5281
|
+
const existed = existsSync4(paths.systemdUnit);
|
|
4801
5282
|
const disable = run("systemctl", ["--user", "disable", "--now", `${SERVICE_LABEL}.service`]);
|
|
4802
5283
|
if (existed)
|
|
4803
|
-
|
|
5284
|
+
rmSync2(paths.systemdUnit, { force: true });
|
|
4804
5285
|
run("systemctl", ["--user", "daemon-reload"]);
|
|
4805
|
-
return {
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
5286
|
+
return { detail: disable.detail };
|
|
5287
|
+
},
|
|
5288
|
+
start(_paths, installed) {
|
|
5289
|
+
const start = installed ? run("systemctl", ["--user", "start", `${SERVICE_LABEL}.service`]) : notInstalled("systemd unit");
|
|
5290
|
+
return { active: start.ok ? true : null, detail: start.detail };
|
|
5291
|
+
},
|
|
5292
|
+
stop(_paths, installed) {
|
|
5293
|
+
const stop = installed ? run("systemctl", ["--user", "stop", `${SERVICE_LABEL}.service`]) : notInstalled("systemd unit");
|
|
5294
|
+
return { detail: stop.detail };
|
|
5295
|
+
},
|
|
5296
|
+
status() {
|
|
5297
|
+
const status = run("systemctl", ["--user", "is-active", `${SERVICE_LABEL}.service`]);
|
|
5298
|
+
return { active: status.ok, detail: status.detail };
|
|
4812
5299
|
}
|
|
4813
|
-
|
|
4814
|
-
|
|
5300
|
+
};
|
|
5301
|
+
var scheduledTask = {
|
|
5302
|
+
unitPath: (paths) => paths.windowsScript,
|
|
5303
|
+
install(paths, runner) {
|
|
5304
|
+
writeFileSync3(paths.windowsScript, renderWindowsPowerShellScript(runner), { mode: 384 });
|
|
5305
|
+
const create = run("schtasks.exe", [
|
|
5306
|
+
"/Create",
|
|
5307
|
+
"/TN",
|
|
5308
|
+
SERVICE_LABEL,
|
|
5309
|
+
"/TR",
|
|
5310
|
+
`powershell.exe -NoProfile -ExecutionPolicy Bypass -File "${paths.windowsScript}"`,
|
|
5311
|
+
"/SC",
|
|
5312
|
+
"ONLOGON",
|
|
5313
|
+
"/F",
|
|
5314
|
+
"/RL",
|
|
5315
|
+
"LIMITED"
|
|
5316
|
+
]);
|
|
5317
|
+
const start = run("schtasks.exe", ["/Run", "/TN", SERVICE_LABEL]);
|
|
5318
|
+
return { installed: create.ok, active: start.ok ? true : null, detail: joinDetails(create, start) };
|
|
5319
|
+
},
|
|
5320
|
+
uninstall(paths) {
|
|
5321
|
+
const existed = existsSync4(paths.windowsScript);
|
|
4815
5322
|
const stop = run("schtasks.exe", ["/End", "/TN", SERVICE_LABEL]);
|
|
4816
5323
|
const del = run("schtasks.exe", ["/Delete", "/TN", SERVICE_LABEL, "/F"]);
|
|
4817
5324
|
if (existed)
|
|
4818
|
-
|
|
4819
|
-
return {
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
5325
|
+
rmSync2(paths.windowsScript, { force: true });
|
|
5326
|
+
return { detail: joinDetails(stop, del) };
|
|
5327
|
+
},
|
|
5328
|
+
start(_paths, installed) {
|
|
5329
|
+
const start = installed ? run("schtasks.exe", ["/Run", "/TN", SERVICE_LABEL]) : notInstalled("Scheduled Task");
|
|
5330
|
+
return { active: start.ok ? true : null, detail: start.detail };
|
|
5331
|
+
},
|
|
5332
|
+
stop(_paths, installed) {
|
|
5333
|
+
const stop = installed ? run("schtasks.exe", ["/End", "/TN", SERVICE_LABEL]) : notInstalled("Scheduled Task");
|
|
5334
|
+
return { detail: stop.detail };
|
|
5335
|
+
},
|
|
5336
|
+
status(paths, installed) {
|
|
5337
|
+
const status = run("schtasks.exe", ["/Query", "/TN", SERVICE_LABEL, "/FO", "LIST", "/V"]);
|
|
5338
|
+
const detail = status.detail || (installed ? readFileSync5(paths.windowsScript, "utf8") : undefined);
|
|
5339
|
+
return { active: status.ok ? /Status:\s*Running/i.test(detail ?? "") : false, detail };
|
|
4827
5340
|
}
|
|
4828
|
-
|
|
5341
|
+
};
|
|
5342
|
+
var DRIVERS = {
|
|
5343
|
+
darwin: launchd,
|
|
5344
|
+
linux: systemd,
|
|
5345
|
+
win32: scheduledTask
|
|
5346
|
+
};
|
|
5347
|
+
var SUPPORTED_PLATFORMS_MESSAGE = "Background services are supported on macOS launchd, Linux systemd user services, and Windows Scheduled Tasks.";
|
|
5348
|
+
function serviceDriver() {
|
|
5349
|
+
return DRIVERS[platform5()];
|
|
4829
5350
|
}
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
}
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
path: paths.systemdUnit,
|
|
4853
|
-
detail: start.detail
|
|
4854
|
-
};
|
|
4855
|
-
}
|
|
4856
|
-
if (platform3() === "win32") {
|
|
4857
|
-
const installed = existsSync3(paths.windowsScript);
|
|
4858
|
-
const start = installed ? run("schtasks.exe", ["/Run", "/TN", SERVICE_LABEL]) : { ok: false, detail: "Scheduled Task is not installed." };
|
|
4859
|
-
return {
|
|
4860
|
-
platform: platform3(),
|
|
4861
|
-
installed,
|
|
4862
|
-
active: start.ok ? true : null,
|
|
4863
|
-
path: paths.windowsScript,
|
|
4864
|
-
detail: start.detail
|
|
4865
|
-
};
|
|
5351
|
+
|
|
5352
|
+
// src/agent/service.ts
|
|
5353
|
+
function isEphemeralRunnerPath(path) {
|
|
5354
|
+
const normalized = path.replace(/\\/g, "/");
|
|
5355
|
+
return normalized.includes("/_npx/") || normalized.includes("/_cacache/") || normalized.includes("/.pnpm-store/") || normalized.includes("/.yarn/$$virtual/");
|
|
5356
|
+
}
|
|
5357
|
+
function vendorRunner(scriptPath, paths = getServicePaths()) {
|
|
5358
|
+
if (!isEphemeralRunnerPath(scriptPath))
|
|
5359
|
+
return scriptPath;
|
|
5360
|
+
mkdirSync3(paths.binDir, { recursive: true, mode: 448 });
|
|
5361
|
+
const source = realpathSync3(scriptPath);
|
|
5362
|
+
copyFileSync(source, paths.vendoredRunner);
|
|
5363
|
+
try {
|
|
5364
|
+
chmodSync3(paths.vendoredRunner, 448);
|
|
5365
|
+
} catch {}
|
|
5366
|
+
writeFileSync4(join9(paths.binDir, "agent-cli.source.json"), JSON.stringify({ source, vendoredFrom: scriptPath }, null, 2), { mode: 384 });
|
|
5367
|
+
return paths.vendoredRunner;
|
|
5368
|
+
}
|
|
5369
|
+
function currentRunnerParts() {
|
|
5370
|
+
const script = process.argv[1];
|
|
5371
|
+
if (script && existsSync5(script)) {
|
|
5372
|
+
return { command: process.execPath, args: [vendorRunner(script), "run", "--service"] };
|
|
4866
5373
|
}
|
|
4867
|
-
throw new Error("
|
|
5374
|
+
throw new Error("Cannot install the background service because the current Agent Tunnel executable was not found");
|
|
4868
5375
|
}
|
|
4869
|
-
function
|
|
5376
|
+
function withDriver(operate, fallback) {
|
|
5377
|
+
const driver = serviceDriver();
|
|
4870
5378
|
const paths = getServicePaths();
|
|
4871
|
-
if (
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
detail: stop.detail
|
|
4902
|
-
};
|
|
4903
|
-
}
|
|
4904
|
-
throw new Error("Background service stop is currently supported on macOS, Linux, and Windows.");
|
|
5379
|
+
if (!driver)
|
|
5380
|
+
throw new Error(SUPPORTED_PLATFORMS_MESSAGE);
|
|
5381
|
+
const path = driver.unitPath(paths);
|
|
5382
|
+
const installed = existsSync5(path);
|
|
5383
|
+
const outcome = operate(driver, paths, installed);
|
|
5384
|
+
return {
|
|
5385
|
+
platform: platform6(),
|
|
5386
|
+
installed: outcome.installed ?? installed,
|
|
5387
|
+
active: outcome.active ?? fallback.active,
|
|
5388
|
+
path,
|
|
5389
|
+
detail: outcome.detail
|
|
5390
|
+
};
|
|
5391
|
+
}
|
|
5392
|
+
function installService() {
|
|
5393
|
+
const paths = getServicePaths();
|
|
5394
|
+
mkdirSync3(paths.configDir, { recursive: true, mode: 448 });
|
|
5395
|
+
mkdirSync3(paths.logDir, { recursive: true, mode: 448 });
|
|
5396
|
+
rotateServiceLogs(paths);
|
|
5397
|
+
const runner = currentRunnerParts();
|
|
5398
|
+
return withDriver((driver, servicePaths) => driver.install(servicePaths, runner), { installed: false, active: null });
|
|
5399
|
+
}
|
|
5400
|
+
function uninstallService() {
|
|
5401
|
+
rmSync3(getServicePaths().binDir, { recursive: true, force: true });
|
|
5402
|
+
return withDriver((driver, paths) => ({ ...driver.uninstall(paths), installed: false, active: false }), { installed: false, active: false });
|
|
5403
|
+
}
|
|
5404
|
+
function startService() {
|
|
5405
|
+
return withDriver((driver, paths, installed) => driver.start(paths, installed), { installed: false, active: null });
|
|
5406
|
+
}
|
|
5407
|
+
function stopService() {
|
|
5408
|
+
return withDriver((driver, paths, installed) => ({ ...driver.stop(paths, installed), active: false }), { installed: false, active: false });
|
|
4905
5409
|
}
|
|
4906
5410
|
function restartService() {
|
|
4907
5411
|
stopService();
|
|
4908
5412
|
return startService();
|
|
4909
5413
|
}
|
|
4910
5414
|
function getServiceStatus() {
|
|
4911
|
-
|
|
4912
|
-
if (platform3() === "darwin") {
|
|
4913
|
-
const installed = existsSync3(paths.launchdPlist);
|
|
4914
|
-
const status = run("launchctl", ["print", `${launchdTarget()}/${SERVICE_LABEL}`]);
|
|
4915
|
-
return {
|
|
4916
|
-
platform: platform3(),
|
|
4917
|
-
installed,
|
|
4918
|
-
active: status.ok,
|
|
4919
|
-
path: paths.launchdPlist,
|
|
4920
|
-
detail: status.detail || (installed ? readFileSync2(paths.launchdPlist, "utf8") : undefined)
|
|
4921
|
-
};
|
|
4922
|
-
}
|
|
4923
|
-
if (platform3() === "linux") {
|
|
4924
|
-
const installed = existsSync3(paths.systemdUnit);
|
|
4925
|
-
const status = run("systemctl", ["--user", "is-active", `${SERVICE_LABEL}.service`]);
|
|
4926
|
-
return {
|
|
4927
|
-
platform: platform3(),
|
|
4928
|
-
installed,
|
|
4929
|
-
active: status.ok,
|
|
4930
|
-
path: paths.systemdUnit,
|
|
4931
|
-
detail: status.detail
|
|
4932
|
-
};
|
|
4933
|
-
}
|
|
4934
|
-
if (platform3() === "win32") {
|
|
4935
|
-
const installed = existsSync3(paths.windowsScript);
|
|
4936
|
-
const status = run("schtasks.exe", ["/Query", "/TN", SERVICE_LABEL, "/FO", "LIST", "/V"]);
|
|
4937
|
-
const detail = status.detail || (installed ? readFileSync2(paths.windowsScript, "utf8") : undefined);
|
|
5415
|
+
if (!serviceDriver()) {
|
|
4938
5416
|
return {
|
|
4939
|
-
platform:
|
|
4940
|
-
installed,
|
|
4941
|
-
active:
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
};
|
|
4945
|
-
}
|
|
4946
|
-
return {
|
|
4947
|
-
platform: platform3(),
|
|
4948
|
-
installed: false,
|
|
4949
|
-
active: null,
|
|
4950
|
-
detail: "Background service status is currently supported on macOS launchd, Linux systemd user services, and Windows Scheduled Tasks."
|
|
4951
|
-
};
|
|
4952
|
-
}
|
|
4953
|
-
|
|
4954
|
-
// src/agent/cli.ts
|
|
4955
|
-
import { hostname as hostname2, platform as platform4, arch as arch2 } from "os";
|
|
4956
|
-
import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync3, renameSync } from "fs";
|
|
4957
|
-
import { join as join6 } from "path";
|
|
4958
|
-
import { homedir as homedir4 } from "os";
|
|
4959
|
-
import { spawn as spawn3 } from "child_process";
|
|
4960
|
-
import { createInterface } from "readline/promises";
|
|
4961
|
-
var c2 = {
|
|
4962
|
-
reset: "\x1B[0m",
|
|
4963
|
-
bold: "\x1B[1m",
|
|
4964
|
-
dim: "\x1B[2m",
|
|
4965
|
-
italic: "\x1B[3m",
|
|
4966
|
-
cyan: "\x1B[36m",
|
|
4967
|
-
blue: "\x1B[34m",
|
|
4968
|
-
green: "\x1B[32m",
|
|
4969
|
-
yellow: "\x1B[33m",
|
|
4970
|
-
red: "\x1B[31m",
|
|
4971
|
-
magenta: "\x1B[35m",
|
|
4972
|
-
white: "\x1B[97m",
|
|
4973
|
-
gray: "\x1B[90m",
|
|
4974
|
-
bgCyan: "\x1B[46m",
|
|
4975
|
-
bgBlue: "\x1B[44m"
|
|
4976
|
-
};
|
|
4977
|
-
function parseArgs(argv) {
|
|
4978
|
-
const command = argv[2] || "help";
|
|
4979
|
-
const flags = {};
|
|
4980
|
-
for (let i = 3;i < argv.length; i++) {
|
|
4981
|
-
const arg = argv[i];
|
|
4982
|
-
if (arg.startsWith("--")) {
|
|
4983
|
-
const key = arg.slice(2);
|
|
4984
|
-
const value = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : "true";
|
|
4985
|
-
flags[key] = value;
|
|
4986
|
-
}
|
|
5417
|
+
platform: platform6(),
|
|
5418
|
+
installed: false,
|
|
5419
|
+
active: null,
|
|
5420
|
+
detail: SUPPORTED_PLATFORMS_MESSAGE
|
|
5421
|
+
};
|
|
4987
5422
|
}
|
|
4988
|
-
return {
|
|
4989
|
-
}
|
|
4990
|
-
function clearScreen() {
|
|
4991
|
-
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
5423
|
+
return withDriver((driver, paths, installed) => driver.status(paths, installed), { installed: false, active: null });
|
|
4992
5424
|
}
|
|
4993
|
-
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4994
|
-
var TUNNEL_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
4995
|
-
var SETUP_TOKEN_PATTERN = /^kortix_tnl_[A-Za-z0-9_-]{32,64}$/;
|
|
4996
5425
|
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5426
|
+
// src/agent/service-control.ts
|
|
5427
|
+
function acquireTunnelLease() {
|
|
5428
|
+
const serviceWasActive = getServiceStatus().active === true;
|
|
5429
|
+
if (serviceWasActive)
|
|
5430
|
+
stopService();
|
|
5431
|
+
let resumed = false;
|
|
5432
|
+
return {
|
|
5433
|
+
serviceWasActive,
|
|
5434
|
+
resumeService() {
|
|
5435
|
+
if (!serviceWasActive || resumed)
|
|
5436
|
+
return;
|
|
5437
|
+
resumed = true;
|
|
5438
|
+
startService();
|
|
5439
|
+
}
|
|
5440
|
+
};
|
|
5002
5441
|
}
|
|
5003
|
-
|
|
5004
|
-
|
|
5442
|
+
var SERVICE_ACTIONS = {
|
|
5443
|
+
start: { run: startService, label: "started" },
|
|
5444
|
+
stop: { run: stopService, label: "stopped" },
|
|
5445
|
+
restart: { run: restartService, label: "restarted" },
|
|
5446
|
+
uninstall: { run: uninstallService, label: "removed" },
|
|
5447
|
+
install: { run: installService, label: "installed" }
|
|
5448
|
+
};
|
|
5449
|
+
function describeService(status) {
|
|
5450
|
+
if (!status.installed)
|
|
5451
|
+
return `${glyph.off} not installed`;
|
|
5452
|
+
if (status.active)
|
|
5453
|
+
return `${glyph.on} running ${c.dim}· starts at login${c.reset}`;
|
|
5454
|
+
return `${c.yellow}○${c.reset} installed ${c.dim}· stopped${c.reset}`;
|
|
5005
5455
|
}
|
|
5006
|
-
function
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
if (
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
return { tunnelId, token };
|
|
5456
|
+
function renderServiceAction(action, status) {
|
|
5457
|
+
blankLine();
|
|
5458
|
+
console.log(` ${glyph.on} ${c.bold}Background service ${SERVICE_ACTIONS[action].label}${c.reset}`);
|
|
5459
|
+
if (status.path)
|
|
5460
|
+
field("", `${c.dim}${status.path}${c.reset}`);
|
|
5461
|
+
if (status.detail)
|
|
5462
|
+
field("", `${c.gray}${status.detail}${c.reset}`);
|
|
5463
|
+
blankLine();
|
|
5015
5464
|
}
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
}
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
const browserUrl = normalizeBrowserUrl(verificationUrl);
|
|
5031
|
-
if (!browserUrl) {
|
|
5032
|
-
throw new InvalidDeviceAuthResponseError("Authorization server returned an invalid verification URL");
|
|
5033
|
-
}
|
|
5034
|
-
const parsedVerificationUrl = new URL(browserUrl);
|
|
5035
|
-
const loopback = parsedVerificationUrl.hostname === "localhost" || parsedVerificationUrl.hostname === "127.0.0.1" || parsedVerificationUrl.hostname === "[::1]" || parsedVerificationUrl.hostname === "::1";
|
|
5036
|
-
if (parsedVerificationUrl.username || parsedVerificationUrl.password || parsedVerificationUrl.protocol !== "https:" && !loopback) {
|
|
5037
|
-
throw new InvalidDeviceAuthResponseError("Authorization server returned an unsafe verification URL");
|
|
5038
|
-
}
|
|
5039
|
-
if (typeof expiresAt !== "string") {
|
|
5040
|
-
throw new InvalidDeviceAuthResponseError("Authorization server returned an invalid expiration");
|
|
5041
|
-
}
|
|
5042
|
-
const expiresAtMs = Date.parse(expiresAt);
|
|
5043
|
-
const now = Date.now();
|
|
5044
|
-
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= now || expiresAtMs > now + 10 * 60000) {
|
|
5045
|
-
throw new InvalidDeviceAuthResponseError("Authorization server returned an invalid expiration");
|
|
5046
|
-
}
|
|
5047
|
-
if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 250 || pollIntervalMs > 1e4) {
|
|
5048
|
-
throw new InvalidDeviceAuthResponseError("Authorization server returned an invalid poll interval");
|
|
5465
|
+
|
|
5466
|
+
// src/agent/cli.ts
|
|
5467
|
+
var ALL_CAPABILITIES = ["filesystem", "shell", "desktop"];
|
|
5468
|
+
var BACKGROUND_FLAGS = ["daemon", "service", "background", "always-online"];
|
|
5469
|
+
var FOREGROUND_FLAGS = ["foreground", "no-daemon", "no-service", "no-background"];
|
|
5470
|
+
var DEFAULT_LOG_LINES = 60;
|
|
5471
|
+
function parseArgs(argv) {
|
|
5472
|
+
const flags = {};
|
|
5473
|
+
for (let i = 3;i < argv.length; i++) {
|
|
5474
|
+
const arg = argv[i];
|
|
5475
|
+
if (!arg.startsWith("--"))
|
|
5476
|
+
continue;
|
|
5477
|
+
const next = argv[i + 1];
|
|
5478
|
+
flags[arg.slice(2)] = next && !next.startsWith("--") ? argv[++i] : "true";
|
|
5049
5479
|
}
|
|
5050
|
-
return {
|
|
5051
|
-
deviceCode,
|
|
5052
|
-
deviceSecret,
|
|
5053
|
-
verificationUrl: browserUrl,
|
|
5054
|
-
expiresAt,
|
|
5055
|
-
pollIntervalMs
|
|
5056
|
-
};
|
|
5480
|
+
return { command: argv[2] || "help", flags };
|
|
5057
5481
|
}
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
const
|
|
5064
|
-
|
|
5065
|
-
console.log("");
|
|
5066
|
-
console.log(` ${c2.cyan}▄▀█ █▀▀ █▀▀ █▄ █ ▀█▀${c2.reset} ${c2.cyan}▀█▀ █ █ █▄ █ █▄ █ █▀▀ █ ${c2.reset}`);
|
|
5067
|
-
console.log(` ${c2.cyan}█▀█ █▄█ ██▄ █ ▀█ █${c2.reset} ${c2.cyan} █ █▄█ █ ▀█ █ ▀█ ██▄ █▄▄${c2.reset}`);
|
|
5068
|
-
console.log("");
|
|
5069
|
-
const barW = 50;
|
|
5070
|
-
const frames = 14;
|
|
5071
|
-
for (let i = 0;i <= frames; i++) {
|
|
5072
|
-
const filled = Math.round(i / frames * barW);
|
|
5073
|
-
const empty = barW - filled;
|
|
5074
|
-
process.stdout.write(`\r ${c2.cyan}◇${c2.reset} ${c2.cyan}${"═".repeat(filled)}${c2.reset}${c2.gray}${"─".repeat(empty)}${c2.reset} `);
|
|
5075
|
-
await sleep2(20);
|
|
5076
|
-
}
|
|
5077
|
-
process.stdout.write(`\r ${c2.cyan}◇ ${"═".repeat(barW)} ◆${c2.reset}
|
|
5078
|
-
`);
|
|
5079
|
-
await sleep2(120);
|
|
5080
|
-
const W = 60;
|
|
5081
|
-
const vLen = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
5082
|
-
const row = (content) => {
|
|
5083
|
-
const pad = Math.max(0, W - vLen(content));
|
|
5084
|
-
console.log(` ${c2.gray}│${c2.reset}${content}${" ".repeat(pad)}${c2.gray}│${c2.reset}`);
|
|
5085
|
-
};
|
|
5086
|
-
const blank = () => console.log(` ${c2.gray}│${c2.reset}${" ".repeat(W)}${c2.gray}│${c2.reset}`);
|
|
5087
|
-
const titleL = ` ${c2.cyan}◆${c2.reset} ${c2.bold}${c2.white}Agent Tunnel${c2.reset}`;
|
|
5088
|
-
const titleR = `${c2.dim}v${version}${c2.reset} `;
|
|
5089
|
-
const titleLLen = 18;
|
|
5090
|
-
const titleRLen = 1 + version.length + 3;
|
|
5091
|
-
const titlePad = Math.max(1, W - titleLLen - titleRLen);
|
|
5092
|
-
const capStr = capabilities.map((name) => `${c2.green}●${c2.reset} ${c2.white}${name}${c2.reset}`).join(" ");
|
|
5093
|
-
const brand = "created by kortix";
|
|
5094
|
-
const brandFill = W - brand.length - 3;
|
|
5095
|
-
console.log("");
|
|
5096
|
-
console.log(` ${c2.gray}╭${"─".repeat(W)}╮${c2.reset}`);
|
|
5097
|
-
blank();
|
|
5098
|
-
row(`${titleL}${" ".repeat(titlePad)}${titleR}`);
|
|
5099
|
-
row(` ${c2.dim}Bridge between AI agents & local machines${c2.reset}`);
|
|
5100
|
-
blank();
|
|
5101
|
-
row(` ${c2.dim}tunnel${c2.reset} ${c2.white}${tunnelDisplay}${c2.reset}`);
|
|
5102
|
-
row(` ${c2.dim}relay${c2.reset} ${c2.white}${apiDisplay}${c2.reset}`);
|
|
5103
|
-
row(` ${c2.dim}machine${c2.reset} ${c2.white}${machineDisplay}${c2.reset} ${c2.dim}(${plat})${c2.reset}`);
|
|
5104
|
-
blank();
|
|
5105
|
-
console.log(` ${c2.gray}╰${"─".repeat(brandFill)} ${c2.dim}created by ${c2.cyan}kortix${c2.reset} ${c2.gray}─╯${c2.reset}`);
|
|
5106
|
-
console.log("");
|
|
5482
|
+
function fail(message) {
|
|
5483
|
+
console.error(` ${glyph.bad} ${message}`);
|
|
5484
|
+
process.exit(1);
|
|
5485
|
+
}
|
|
5486
|
+
function shortenHomePath(path) {
|
|
5487
|
+
const home = process.env.HOME ?? "";
|
|
5488
|
+
return home && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
5107
5489
|
}
|
|
5108
5490
|
function startAgent(config, options = {}) {
|
|
5109
5491
|
const registry = createEnabledCapabilityRegistry(config);
|
|
5110
5492
|
if (config.enabledCapabilities?.includes("desktop") && !registry.has("desktop")) {
|
|
5111
5493
|
console.error("[agent-tunnel] Computer Use is approved but unavailable: install the trusted cua-driver locally, then restart Agent Tunnel.");
|
|
5112
5494
|
}
|
|
5113
|
-
if (
|
|
5114
|
-
clearScreen();
|
|
5115
|
-
printStartup(config, registry.getCapabilityNames(), "0.1.2");
|
|
5116
|
-
} else {
|
|
5495
|
+
if (options.service) {
|
|
5117
5496
|
console.log(`[agent-tunnel] service starting: ${config.tunnelId} -> ${config.apiUrl}`);
|
|
5497
|
+
} else {
|
|
5498
|
+
clearScreen();
|
|
5499
|
+
printStartupBanner({
|
|
5500
|
+
tunnelId: config.tunnelId,
|
|
5501
|
+
apiUrl: config.apiUrl,
|
|
5502
|
+
capabilities: registry.getCapabilityNames(),
|
|
5503
|
+
version: agentTunnelVersion()
|
|
5504
|
+
});
|
|
5118
5505
|
}
|
|
5119
|
-
const agent = new TunnelAgent(config, registry
|
|
5506
|
+
const agent = new TunnelAgent(config, registry, {
|
|
5507
|
+
onTerminalClose: ({ reason }) => {
|
|
5508
|
+
if (!options.service)
|
|
5509
|
+
return;
|
|
5510
|
+
console.log(`[agent-tunnel] stopping service: ${reason}`);
|
|
5511
|
+
process.exit(TERMINAL_SERVICE_EXIT_CODE);
|
|
5512
|
+
}
|
|
5513
|
+
});
|
|
5120
5514
|
agent.connect();
|
|
5121
5515
|
const shutdown = () => {
|
|
5122
5516
|
if (!options.service)
|
|
5123
5517
|
console.log(`
|
|
5124
|
-
${
|
|
5518
|
+
${c.dim} Shutting down…${c.reset}`);
|
|
5125
5519
|
agent.disconnect();
|
|
5126
5520
|
process.exit(0);
|
|
5127
5521
|
};
|
|
5128
5522
|
process.on("SIGTERM", shutdown);
|
|
5129
5523
|
process.on("SIGINT", shutdown);
|
|
5130
5524
|
}
|
|
5131
|
-
function
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5525
|
+
async function chooseBackgroundMode(flags) {
|
|
5526
|
+
if (anyFlag(flags, BACKGROUND_FLAGS))
|
|
5527
|
+
return true;
|
|
5528
|
+
if (anyFlag(flags, FOREGROUND_FLAGS))
|
|
5529
|
+
return false;
|
|
5530
|
+
if (!isInteractiveTerminal())
|
|
5531
|
+
return false;
|
|
5532
|
+
blankLine();
|
|
5533
|
+
console.log(` ${glyph.warn} ${c.bold}Security note${c.reset}`);
|
|
5534
|
+
console.log(` ${c.dim}Background mode starts at login, continues after this terminal closes, and restarts after failures.${c.reset}`);
|
|
5535
|
+
console.log(` ${c.dim}The computer must remain powered on, awake, and connected to the internet.${c.reset}`);
|
|
5536
|
+
blankLine();
|
|
5537
|
+
return promptYesNo(" Install the background service now?", DEFAULT_INSTALL_BACKGROUND_SERVICE);
|
|
5138
5538
|
}
|
|
5139
|
-
function
|
|
5140
|
-
if (
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
if (!safeUrl)
|
|
5539
|
+
async function launch(config, flags, lease) {
|
|
5540
|
+
if (await chooseBackgroundMode(flags)) {
|
|
5541
|
+
saveCredentials(config.tunnelId, config.token, config.apiUrl);
|
|
5542
|
+
renderServiceAction("install", SERVICE_ACTIONS.install.run());
|
|
5144
5543
|
return;
|
|
5145
|
-
try {
|
|
5146
|
-
const plat = platform4();
|
|
5147
|
-
let command;
|
|
5148
|
-
let args;
|
|
5149
|
-
if (plat === "darwin") {
|
|
5150
|
-
command = "open";
|
|
5151
|
-
args = [safeUrl];
|
|
5152
|
-
} else if (plat === "win32") {
|
|
5153
|
-
command = "rundll32.exe";
|
|
5154
|
-
args = ["url.dll,FileProtocolHandler", safeUrl];
|
|
5155
|
-
} else {
|
|
5156
|
-
command = "xdg-open";
|
|
5157
|
-
args = [safeUrl];
|
|
5158
|
-
}
|
|
5159
|
-
const child = spawn3(command, args, { detached: true, stdio: "ignore" });
|
|
5160
|
-
child.unref();
|
|
5161
|
-
} catch {}
|
|
5162
|
-
}
|
|
5163
|
-
var CONFIG_DIR2 = join6(homedir4(), ".agent-tunnel");
|
|
5164
|
-
var CONFIG_FILE2 = join6(CONFIG_DIR2, "config.json");
|
|
5165
|
-
function isSetupTunnelToken(token) {
|
|
5166
|
-
return token.startsWith("kortix_tnl_") || token.startsWith("tnl_");
|
|
5167
|
-
}
|
|
5168
|
-
function isTruthyFlag(value) {
|
|
5169
|
-
return value === "true" || value === "1" || value === "yes";
|
|
5170
|
-
}
|
|
5171
|
-
function isInteractiveTerminal() {
|
|
5172
|
-
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
5173
|
-
}
|
|
5174
|
-
async function promptYesNo(question, defaultValue) {
|
|
5175
|
-
const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
|
|
5176
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
5177
|
-
try {
|
|
5178
|
-
for (;; ) {
|
|
5179
|
-
const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase();
|
|
5180
|
-
if (!answer)
|
|
5181
|
-
return defaultValue;
|
|
5182
|
-
if (["y", "yes"].includes(answer))
|
|
5183
|
-
return true;
|
|
5184
|
-
if (["n", "no"].includes(answer))
|
|
5185
|
-
return false;
|
|
5186
|
-
console.log(` ${c2.yellow}!${c2.reset} Please answer yes or no.`);
|
|
5187
|
-
}
|
|
5188
|
-
} catch (error) {
|
|
5189
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
5190
|
-
process.stdout.write(`
|
|
5191
|
-
`);
|
|
5192
|
-
process.exit(130);
|
|
5193
|
-
}
|
|
5194
|
-
throw error;
|
|
5195
|
-
} finally {
|
|
5196
|
-
rl.close();
|
|
5197
|
-
}
|
|
5198
|
-
}
|
|
5199
|
-
async function chooseConnectMode(flags) {
|
|
5200
|
-
const explicitBackground = isTruthyFlag(flags.daemon) || isTruthyFlag(flags.service) || isTruthyFlag(flags.background) || isTruthyFlag(flags["always-online"]);
|
|
5201
|
-
const explicitForeground = isTruthyFlag(flags.foreground) || isTruthyFlag(flags["no-daemon"]) || isTruthyFlag(flags["no-service"]) || isTruthyFlag(flags["no-background"]);
|
|
5202
|
-
if (explicitBackground) {
|
|
5203
|
-
return { background: true };
|
|
5204
5544
|
}
|
|
5205
|
-
if (
|
|
5206
|
-
|
|
5545
|
+
if (lease?.serviceWasActive) {
|
|
5546
|
+
console.log(` ${c.dim}Background service stays paused while this terminal holds the tunnel.${c.reset}`);
|
|
5547
|
+
console.log(` ${c.dim}Resume it with${c.reset} ${c.white}agent-tunnel start${c.reset}${c.dim}, or leave it — it starts again at login.${c.reset}`);
|
|
5207
5548
|
}
|
|
5208
|
-
|
|
5209
|
-
return { background: false };
|
|
5210
|
-
}
|
|
5211
|
-
console.log("");
|
|
5212
|
-
console.log(` ${c2.yellow}!${c2.reset} ${c2.bold}Security note${c2.reset}`);
|
|
5213
|
-
console.log(` ${c2.dim}Background mode starts at login, continues after this terminal closes, and restarts after failures.${c2.reset}`);
|
|
5214
|
-
console.log(` ${c2.dim}The computer must remain powered on, awake, and connected to the internet.${c2.reset}`);
|
|
5215
|
-
console.log("");
|
|
5216
|
-
const background = await promptYesNo(" Install the background service now?", DEFAULT_INSTALL_BACKGROUND_SERVICE);
|
|
5217
|
-
return { background };
|
|
5218
|
-
}
|
|
5219
|
-
function installBackgroundService() {
|
|
5220
|
-
const status = installService();
|
|
5221
|
-
console.log("");
|
|
5222
|
-
console.log(` ${c2.green}●${c2.reset} ${c2.bold}Background service installed${c2.reset}`);
|
|
5223
|
-
if (status.path)
|
|
5224
|
-
console.log(` ${c2.dim}${status.path}${c2.reset}`);
|
|
5225
|
-
console.log(` ${c2.dim}Starts at login and restarts after failures.${c2.reset}`);
|
|
5226
|
-
if (status.detail)
|
|
5227
|
-
console.log(` ${c2.gray}${status.detail}${c2.reset}`);
|
|
5228
|
-
console.log("");
|
|
5549
|
+
startAgent(config);
|
|
5229
5550
|
}
|
|
5230
|
-
function
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
let existing = {};
|
|
5236
|
-
if (existsSync4(CONFIG_FILE2)) {
|
|
5237
|
-
try {
|
|
5238
|
-
existing = JSON.parse(readFileSync3(CONFIG_FILE2, "utf-8"));
|
|
5239
|
-
} catch {}
|
|
5240
|
-
}
|
|
5241
|
-
const tmpFile = join6(CONFIG_DIR2, `config.${process.pid}.${Date.now()}.tmp`);
|
|
5242
|
-
const next = {
|
|
5243
|
-
...existing,
|
|
5244
|
-
tunnelId,
|
|
5245
|
-
token,
|
|
5246
|
-
apiUrl,
|
|
5247
|
-
...enabledCapabilities !== undefined ? { enabledCapabilities } : {}
|
|
5248
|
-
};
|
|
5249
|
-
writeFileSync2(tmpFile, JSON.stringify(next, null, 2), { mode: 384, flag: "wx" });
|
|
5250
|
-
try {
|
|
5251
|
-
chmodSync2(tmpFile, 384);
|
|
5252
|
-
} catch {}
|
|
5253
|
-
renameSync(tmpFile, CONFIG_FILE2);
|
|
5551
|
+
async function pairThisMachine(apiUrl, flags) {
|
|
5552
|
+
blankLine();
|
|
5553
|
+
console.log(` ${glyph.mark} ${c.bold}Device Authorization${c.reset}`);
|
|
5554
|
+
blankLine();
|
|
5555
|
+
let challenge;
|
|
5254
5556
|
try {
|
|
5255
|
-
|
|
5256
|
-
} catch {
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
console.log(
|
|
5260
|
-
|
|
5261
|
-
console.log(
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
let
|
|
5266
|
-
let pollIntervalMs;
|
|
5557
|
+
challenge = await requestDeviceAuthorization(apiUrl);
|
|
5558
|
+
} catch (error) {
|
|
5559
|
+
fail(error instanceof InvalidDeviceAuthResponseError ? error.message : "Failed to start device authorization");
|
|
5560
|
+
}
|
|
5561
|
+
console.log(` ${c.dim}Code:${c.reset} ${c.bold}${c.white}${challenge.deviceCode}${c.reset}`);
|
|
5562
|
+
blankLine();
|
|
5563
|
+
console.log(` ${c.dim}Open this URL on any device to approve:${c.reset}`);
|
|
5564
|
+
console.log(` ${c.cyan}${challenge.verificationUrl}${c.reset}`);
|
|
5565
|
+
blankLine();
|
|
5566
|
+
openBrowser(challenge.verificationUrl);
|
|
5567
|
+
let outcome;
|
|
5267
5568
|
try {
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5569
|
+
outcome = await awaitDeviceAuthorization(apiUrl, challenge, {
|
|
5570
|
+
onWaiting: (secondsRemaining) => {
|
|
5571
|
+
const minutes = Math.floor(secondsRemaining / 60);
|
|
5572
|
+
const seconds = String(secondsRemaining % 60).padStart(2, "0");
|
|
5573
|
+
process.stdout.write(`\r ${c.dim}Waiting for approval... ${c.white}${minutes}:${seconds}${c.reset} `);
|
|
5574
|
+
}
|
|
5272
5575
|
});
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
}
|
|
5285
|
-
|
|
5286
|
-
console.
|
|
5576
|
+
} catch (error) {
|
|
5577
|
+
process.stdout.write(`\r${" ".repeat(60)}\r`);
|
|
5578
|
+
fail(error instanceof Error ? error.message : "Device authorization failed");
|
|
5579
|
+
}
|
|
5580
|
+
process.stdout.write(`\r${" ".repeat(60)}\r`);
|
|
5581
|
+
if (outcome.status === "denied")
|
|
5582
|
+
fail("Authorization denied.");
|
|
5583
|
+
if (outcome.status === "expired")
|
|
5584
|
+
fail("Authorization expired. Please try again.");
|
|
5585
|
+
if (outcome.status === "approved-without-token") {
|
|
5586
|
+
fail("Authorization was approved, but the setup token was not available. Run connect again.");
|
|
5587
|
+
}
|
|
5588
|
+
if (outcome.capabilities.length === 0) {
|
|
5589
|
+
console.log(` ${glyph.bad} ${c.bold}No capabilities were approved${c.reset}`);
|
|
5590
|
+
blankLine();
|
|
5591
|
+
console.log(` ${c.dim}A tunnel with no capabilities connects but cannot act, and the${c.reset}`);
|
|
5592
|
+
console.log(` ${c.dim}approved set can only be changed by pairing again. Nothing was saved.${c.reset}`);
|
|
5593
|
+
blankLine();
|
|
5594
|
+
console.log(` ${c.dim}Run connect again and approve at least one of${c.reset} ${c.white}${ALL_CAPABILITIES.join(", ")}${c.reset}${c.dim}.${c.reset}`);
|
|
5595
|
+
blankLine();
|
|
5287
5596
|
process.exit(1);
|
|
5288
|
-
return;
|
|
5289
|
-
}
|
|
5290
|
-
console.log(` ${c2.dim}Code:${c2.reset} ${c2.bold}${c2.white}${deviceCode}${c2.reset}`);
|
|
5291
|
-
console.log("");
|
|
5292
|
-
console.log(` ${c2.dim}Open this URL on any device to approve:${c2.reset}`);
|
|
5293
|
-
console.log(` ${c2.cyan}${verificationUrl}${c2.reset}`);
|
|
5294
|
-
console.log("");
|
|
5295
|
-
openBrowser(verificationUrl);
|
|
5296
|
-
const expiresAtMs = new Date(expiresAt).getTime();
|
|
5297
|
-
while (true) {
|
|
5298
|
-
const remaining = Math.max(0, Math.floor((expiresAtMs - Date.now()) / 1000));
|
|
5299
|
-
if (remaining <= 0) {
|
|
5300
|
-
console.log(`
|
|
5301
|
-
${c2.red}✗${c2.reset} Authorization expired. Please try again.`);
|
|
5302
|
-
process.exit(1);
|
|
5303
|
-
}
|
|
5304
|
-
const min = Math.floor(remaining / 60);
|
|
5305
|
-
const sec = remaining % 60;
|
|
5306
|
-
process.stdout.write(`\r ${c2.dim}Waiting for approval... ${c2.white}${min}:${sec.toString().padStart(2, "0")}${c2.reset} `);
|
|
5307
|
-
try {
|
|
5308
|
-
const res = await fetch(`${config.apiUrl}/device-auth/${deviceCode}/status`, {
|
|
5309
|
-
headers: { Authorization: `Bearer ${deviceSecret}` }
|
|
5310
|
-
});
|
|
5311
|
-
if (res.ok) {
|
|
5312
|
-
const data = await res.json();
|
|
5313
|
-
if (!isJsonRecord(data) || typeof data.status !== "string") {
|
|
5314
|
-
throw new InvalidDeviceAuthResponseError("Authorization server returned an invalid status response");
|
|
5315
|
-
}
|
|
5316
|
-
if (data.status === "approved" && data.tunnelId && data.token) {
|
|
5317
|
-
const credentials = parseApprovedDeviceCredentials(data);
|
|
5318
|
-
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
5319
|
-
console.log(` ${c2.green}●${c2.reset} ${c2.bold}Authorized!${c2.reset}`);
|
|
5320
|
-
console.log("");
|
|
5321
|
-
const enabledCapabilities = Array.isArray(data.capabilities) ? [...new Set(data.capabilities)].filter((capability) => typeof capability === "string" && ["filesystem", "shell", "desktop"].includes(capability)) : [];
|
|
5322
|
-
saveCredentials(credentials.tunnelId, credentials.token, config.apiUrl, enabledCapabilities);
|
|
5323
|
-
console.log(` ${c2.dim}Credentials saved to ${CONFIG_FILE2}${c2.reset}`);
|
|
5324
|
-
console.log(` ${c2.dim}Local capabilities: ${enabledCapabilities.join(", ") || "none"}${c2.reset}`);
|
|
5325
|
-
console.log("");
|
|
5326
|
-
const mode = await chooseConnectMode(flags);
|
|
5327
|
-
if (mode.background) {
|
|
5328
|
-
installBackgroundService();
|
|
5329
|
-
return;
|
|
5330
|
-
}
|
|
5331
|
-
const fullConfig = loadConfig({
|
|
5332
|
-
token: credentials.token,
|
|
5333
|
-
tunnelId: credentials.tunnelId,
|
|
5334
|
-
apiUrl: config.apiUrl
|
|
5335
|
-
});
|
|
5336
|
-
startAgent(fullConfig);
|
|
5337
|
-
return;
|
|
5338
|
-
}
|
|
5339
|
-
if (data.status === "approved") {
|
|
5340
|
-
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
5341
|
-
console.log(` ${c2.red}✗${c2.reset} Authorization was approved, but the setup token was not available.`);
|
|
5342
|
-
console.log(` ${c2.dim}Run the connect command again to create a fresh device authorization code.${c2.reset}`);
|
|
5343
|
-
process.exit(1);
|
|
5344
|
-
}
|
|
5345
|
-
if (data.status === "denied") {
|
|
5346
|
-
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
5347
|
-
console.log(` ${c2.red}✗${c2.reset} Authorization denied.`);
|
|
5348
|
-
process.exit(1);
|
|
5349
|
-
}
|
|
5350
|
-
if (data.status === "expired") {
|
|
5351
|
-
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
5352
|
-
console.log(` ${c2.red}✗${c2.reset} Authorization expired. Please try again.`);
|
|
5353
|
-
process.exit(1);
|
|
5354
|
-
}
|
|
5355
|
-
}
|
|
5356
|
-
} catch (error) {
|
|
5357
|
-
if (error instanceof InvalidDeviceAuthResponseError) {
|
|
5358
|
-
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
5359
|
-
console.error(` ${c2.red}✗${c2.reset} ${error.message}`);
|
|
5360
|
-
process.exit(1);
|
|
5361
|
-
}
|
|
5362
|
-
}
|
|
5363
|
-
await sleep2(pollIntervalMs);
|
|
5364
5597
|
}
|
|
5598
|
+
console.log(` ${glyph.on} ${c.bold}Authorized${c.reset}`);
|
|
5599
|
+
saveCredentials(outcome.tunnelId, outcome.token, apiUrl, outcome.capabilities);
|
|
5600
|
+
console.log(` ${c.dim}Saved to ${CONFIG_FILE2}${c.reset}`);
|
|
5601
|
+
console.log(` ${c.dim}Access: ${outcome.capabilities.join(", ")}${c.reset}`);
|
|
5602
|
+
blankLine();
|
|
5603
|
+
await launch(loadConfig({ apiUrl }), flags);
|
|
5365
5604
|
}
|
|
5366
5605
|
async function commandConnect(flags) {
|
|
5367
5606
|
const config = loadConfig({
|
|
@@ -5369,195 +5608,260 @@ async function commandConnect(flags) {
|
|
|
5369
5608
|
tunnelId: flags["tunnel-id"],
|
|
5370
5609
|
apiUrl: flags["api-url"]
|
|
5371
5610
|
});
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
}
|
|
5379
|
-
startAgent(config);
|
|
5611
|
+
const explicitCredentials = Boolean(flags.token && flags["tunnel-id"]);
|
|
5612
|
+
if (Boolean(config.token) !== Boolean(config.tunnelId)) {
|
|
5613
|
+
fail("Provide both --token and --tunnel-id, or neither (for device auth)");
|
|
5614
|
+
}
|
|
5615
|
+
if (!config.token) {
|
|
5616
|
+
await pairThisMachine(config.apiUrl, flags);
|
|
5380
5617
|
return;
|
|
5381
5618
|
}
|
|
5382
|
-
if (
|
|
5383
|
-
|
|
5619
|
+
if (isTruthyFlag(flags.reauth) && !explicitCredentials) {
|
|
5620
|
+
clearSavedCredentials();
|
|
5621
|
+
await pairThisMachine(config.apiUrl, flags);
|
|
5384
5622
|
return;
|
|
5385
5623
|
}
|
|
5386
|
-
|
|
5387
|
-
|
|
5624
|
+
const lease = acquireTunnelLease();
|
|
5625
|
+
blankLine();
|
|
5626
|
+
console.log(` ${glyph.mark} ${c.dim}Checking saved credentials…${c.reset}`);
|
|
5627
|
+
let probe;
|
|
5628
|
+
try {
|
|
5629
|
+
probe = await probeCredentials(config, {
|
|
5630
|
+
capabilities: createEnabledCapabilityRegistry(config).getCapabilityNames()
|
|
5631
|
+
});
|
|
5632
|
+
} catch (error) {
|
|
5633
|
+
lease.resumeService();
|
|
5634
|
+
throw error;
|
|
5635
|
+
}
|
|
5636
|
+
if (probe === "unreachable") {
|
|
5637
|
+
lease.resumeService();
|
|
5638
|
+
fail(`Cannot reach the relay at ${config.apiUrl}. Check your network, then run connect again.`);
|
|
5639
|
+
}
|
|
5640
|
+
if (probe === "rejected") {
|
|
5641
|
+
if (explicitCredentials)
|
|
5642
|
+
fail("The supplied --token was rejected for this tunnel.");
|
|
5643
|
+
console.log(` ${glyph.warn} ${c.dim}Saved token rejected — re-authorizing${c.reset}`);
|
|
5644
|
+
clearSavedCredentials();
|
|
5645
|
+
await pairThisMachine(config.apiUrl, flags);
|
|
5646
|
+
return;
|
|
5647
|
+
}
|
|
5648
|
+
await launch(config, flags, lease);
|
|
5388
5649
|
}
|
|
5389
|
-
|
|
5650
|
+
function commandRun(flags) {
|
|
5390
5651
|
const config = loadConfig({
|
|
5391
5652
|
token: flags.token,
|
|
5392
5653
|
tunnelId: flags["tunnel-id"],
|
|
5393
5654
|
apiUrl: flags["api-url"]
|
|
5394
5655
|
});
|
|
5656
|
+
const asService = flags.service === "true";
|
|
5395
5657
|
if (!config.token || !config.tunnelId) {
|
|
5396
|
-
console.error(
|
|
5397
|
-
process.exit(1);
|
|
5658
|
+
console.error(` ${glyph.bad} No saved tunnel credentials found. Run \`agent-tunnel connect\` first.`);
|
|
5659
|
+
process.exit(asService ? TERMINAL_SERVICE_EXIT_CODE : 1);
|
|
5398
5660
|
}
|
|
5399
|
-
|
|
5661
|
+
if (asService)
|
|
5662
|
+
rotateServiceLogs();
|
|
5663
|
+
startAgent(config, { service: asService });
|
|
5400
5664
|
}
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
if (!config.token || !config.tunnelId) {
|
|
5408
|
-
console.error("Error: --token and --tunnel-id are required");
|
|
5409
|
-
process.exit(1);
|
|
5665
|
+
function lastServiceActivity() {
|
|
5666
|
+
try {
|
|
5667
|
+
const lines = readFileSync6(join10(getServicePaths().logDir, "agent-tunnel.out.log"), "utf8").split(/\r?\n/).map((line) => stripAnsi(line).trim()).filter((line) => line.length > 0);
|
|
5668
|
+
return lines.at(-1) ?? null;
|
|
5669
|
+
} catch {
|
|
5670
|
+
return null;
|
|
5410
5671
|
}
|
|
5411
|
-
|
|
5672
|
+
}
|
|
5673
|
+
function commandStatus(flags) {
|
|
5674
|
+
const config = loadConfig({ apiUrl: flags["api-url"] });
|
|
5675
|
+
const service = getServiceStatus();
|
|
5676
|
+
const paired = Boolean(config.token && config.tunnelId);
|
|
5677
|
+
const approved = new Set(config.enabledCapabilities ?? []);
|
|
5678
|
+
if (isTruthyFlag(flags.json)) {
|
|
5412
5679
|
console.log(JSON.stringify({
|
|
5413
|
-
|
|
5680
|
+
paired,
|
|
5681
|
+
tunnelId: paired ? config.tunnelId : null,
|
|
5414
5682
|
apiUrl: config.apiUrl,
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
service
|
|
5683
|
+
capabilities: [...approved],
|
|
5684
|
+
version: agentTunnelVersion(),
|
|
5685
|
+
service,
|
|
5686
|
+
lastActivity: lastServiceActivity()
|
|
5418
5687
|
}, null, 2));
|
|
5419
5688
|
return;
|
|
5420
5689
|
}
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
console.log(JSON.stringify(data, null, 2));
|
|
5431
|
-
} catch (err) {
|
|
5432
|
-
console.error("Error:", err);
|
|
5433
|
-
process.exit(1);
|
|
5434
|
-
}
|
|
5435
|
-
}
|
|
5436
|
-
function commandInstallService(flags) {
|
|
5437
|
-
const config = loadConfig({
|
|
5438
|
-
token: flags.token,
|
|
5439
|
-
tunnelId: flags["tunnel-id"],
|
|
5440
|
-
apiUrl: flags["api-url"]
|
|
5441
|
-
});
|
|
5442
|
-
if (!config.token || !config.tunnelId) {
|
|
5443
|
-
console.error(`${c2.red}${c2.bold} error${c2.reset} No saved tunnel credentials found. Run \`agent-tunnel connect\` first, or pass --token and --tunnel-id.`);
|
|
5444
|
-
process.exit(1);
|
|
5445
|
-
}
|
|
5446
|
-
if (flags.token && flags["tunnel-id"]) {
|
|
5447
|
-
saveCredentials(config.tunnelId, config.token, config.apiUrl);
|
|
5690
|
+
blankLine();
|
|
5691
|
+
console.log(` ${glyph.mark} ${c.bold}${c.white}Agent Tunnel${c.reset} ${c.dim}v${agentTunnelVersion()}${c.reset} ${c.dim}${hostname4()}${c.reset}`);
|
|
5692
|
+
blankLine();
|
|
5693
|
+
if (!paired) {
|
|
5694
|
+
console.log(` ${glyph.off} ${c.bold}Not paired${c.reset}`);
|
|
5695
|
+
blankLine();
|
|
5696
|
+
console.log(` ${c.dim}Pair this machine:${c.reset} ${c.white}agent-tunnel connect --api-url <url>${c.reset}`);
|
|
5697
|
+
blankLine();
|
|
5698
|
+
return;
|
|
5448
5699
|
}
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
}
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5700
|
+
field("tunnel", `${c.white}${config.tunnelId}${c.reset}`);
|
|
5701
|
+
field("relay", `${c.white}${config.apiUrl}${c.reset}`);
|
|
5702
|
+
field("capabilities", ALL_CAPABILITIES.map((name) => approved.has(name) ? `${glyph.on} ${c.white}${name}${c.reset}` : `${c.gray}○ ${name}${c.reset}`).join(" "));
|
|
5703
|
+
blankLine();
|
|
5704
|
+
field("service", describeService(service));
|
|
5705
|
+
if (service.installed && service.path)
|
|
5706
|
+
field("", `${c.dim}${shortenHomePath(service.path)}${c.reset}`);
|
|
5707
|
+
const activity = lastServiceActivity();
|
|
5708
|
+
if (activity)
|
|
5709
|
+
field("last log", `${c.dim}${activity}${c.reset}`);
|
|
5710
|
+
blankLine();
|
|
5711
|
+
if (approved.size === 0) {
|
|
5712
|
+
console.log(` ${glyph.warn} ${c.dim}No capabilities approved — this tunnel cannot act.${c.reset}`);
|
|
5713
|
+
console.log(` ${c.dim}Pair again with${c.reset} ${c.white}agent-tunnel connect --reauth${c.reset}`);
|
|
5714
|
+
blankLine();
|
|
5715
|
+
}
|
|
5716
|
+
console.log(` ${c.dim}Recent logs:${c.reset} ${c.white}agent-tunnel logs${c.reset}`);
|
|
5717
|
+
blankLine();
|
|
5457
5718
|
}
|
|
5458
|
-
function
|
|
5459
|
-
|
|
5719
|
+
function commandLogout(flags) {
|
|
5720
|
+
const removed = clearSavedCredentials();
|
|
5721
|
+
const keepService = isTruthyFlag(flags["keep-service"]);
|
|
5722
|
+
if (!keepService)
|
|
5723
|
+
SERVICE_ACTIONS.uninstall.run();
|
|
5724
|
+
blankLine();
|
|
5725
|
+
console.log(removed ? ` ${glyph.on} ${c.bold}Signed out${c.reset} ${c.dim}(credentials cleared from ${CONFIG_FILE2})${c.reset}` : ` ${glyph.off} ${c.dim}No saved credentials to clear${c.reset}`);
|
|
5726
|
+
console.log(keepService ? ` ${glyph.warn} ${c.dim}Background service kept — it cannot authenticate until you connect again${c.reset}` : ` ${c.dim}Background service removed${c.reset}`);
|
|
5727
|
+
blankLine();
|
|
5728
|
+
console.log(` ${c.dim}Pair again with:${c.reset} ${c.white}agent-tunnel connect --api-url <url>${c.reset}`);
|
|
5729
|
+
blankLine();
|
|
5460
5730
|
}
|
|
5461
|
-
function
|
|
5462
|
-
console.log(JSON.stringify(restartService(), null, 2));
|
|
5463
|
-
}
|
|
5464
|
-
function commandServiceStatus() {
|
|
5465
|
-
console.log(JSON.stringify(getServiceStatus(), null, 2));
|
|
5466
|
-
}
|
|
5467
|
-
function commandLogs() {
|
|
5731
|
+
function commandLogs(flags) {
|
|
5468
5732
|
const paths = getServicePaths();
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5733
|
+
if (isTruthyFlag(flags.clear)) {
|
|
5734
|
+
for (const file of serviceLogFiles(paths)) {
|
|
5735
|
+
try {
|
|
5736
|
+
writeFileSync5(file, "", { mode: 384 });
|
|
5737
|
+
} catch {}
|
|
5738
|
+
}
|
|
5739
|
+
blankLine();
|
|
5740
|
+
console.log(` ${glyph.on} ${c.dim}Service logs cleared${c.reset}`);
|
|
5741
|
+
blankLine();
|
|
5742
|
+
return;
|
|
5743
|
+
}
|
|
5744
|
+
const requested = Number.parseInt(flags.lines ?? "", 10);
|
|
5745
|
+
const limit = Number.isSafeInteger(requested) && requested > 0 ? requested : DEFAULT_LOG_LINES;
|
|
5746
|
+
const showAll = isTruthyFlag(flags.all);
|
|
5747
|
+
for (const [label, file] of [
|
|
5748
|
+
["output", join10(paths.logDir, "agent-tunnel.out.log")],
|
|
5749
|
+
["errors", join10(paths.logDir, "agent-tunnel.err.log")]
|
|
5750
|
+
]) {
|
|
5751
|
+
blankLine();
|
|
5752
|
+
console.log(` ${c.bold}${c.white}${label}${c.reset} ${c.dim}${shortenHomePath(file)}${c.reset}`);
|
|
5753
|
+
if (!existsSync6(file)) {
|
|
5754
|
+
console.log(` ${c.dim}not created yet${c.reset}`);
|
|
5755
|
+
continue;
|
|
5756
|
+
}
|
|
5757
|
+
const kept = readFileSync6(file, "utf8").split(/\r?\n/).map((line) => line.trimEnd()).filter((line) => line.trim().length > 0).filter((line) => showAll || !isShellStartupNoise(line));
|
|
5758
|
+
const lines = collapseRepeatedLines(kept).slice(-limit);
|
|
5759
|
+
if (lines.length === 0) {
|
|
5760
|
+
console.log(` ${c.dim}empty${c.reset}`);
|
|
5478
5761
|
continue;
|
|
5479
5762
|
}
|
|
5480
|
-
const
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5763
|
+
for (const line of lines)
|
|
5764
|
+
console.log(` ${line}`);
|
|
5765
|
+
}
|
|
5766
|
+
blankLine();
|
|
5767
|
+
console.log(` ${c.dim}--lines <n> to show more, --all to keep shell noise, --clear to empty them.${c.reset}`);
|
|
5768
|
+
blankLine();
|
|
5769
|
+
}
|
|
5770
|
+
function commandServiceAction(action, flags) {
|
|
5771
|
+
if (action === "install") {
|
|
5772
|
+
const config = loadConfig({
|
|
5773
|
+
token: flags.token,
|
|
5774
|
+
tunnelId: flags["tunnel-id"],
|
|
5775
|
+
apiUrl: flags["api-url"]
|
|
5776
|
+
});
|
|
5777
|
+
if (!config.token || !config.tunnelId) {
|
|
5778
|
+
fail("No saved tunnel credentials found. Run `agent-tunnel connect` first, or pass --token and --tunnel-id.");
|
|
5779
|
+
}
|
|
5780
|
+
if (flags.token && flags["tunnel-id"]) {
|
|
5781
|
+
saveCredentials(config.tunnelId, config.token, config.apiUrl);
|
|
5782
|
+
}
|
|
5484
5783
|
}
|
|
5784
|
+
renderServiceAction(action, SERVICE_ACTIONS[action].run());
|
|
5485
5785
|
}
|
|
5786
|
+
var COMMANDS = {
|
|
5787
|
+
connect: {
|
|
5788
|
+
summary: "Pair this machine, then run it in the background or this terminal",
|
|
5789
|
+
run: commandConnect
|
|
5790
|
+
},
|
|
5791
|
+
status: { summary: "Show pairing, capabilities, and service state (--json)", run: commandStatus },
|
|
5792
|
+
logs: { summary: "Show recent service logs (--lines <n>, --all, --clear)", run: commandLogs },
|
|
5793
|
+
start: { summary: "Start the background service", run: (f) => commandServiceAction("start", f) },
|
|
5794
|
+
stop: {
|
|
5795
|
+
summary: "Stop the background service (keeps it installed)",
|
|
5796
|
+
run: (f) => commandServiceAction("stop", f),
|
|
5797
|
+
aliases: ["disable"]
|
|
5798
|
+
},
|
|
5799
|
+
restart: { summary: "Restart the background service", run: (f) => commandServiceAction("restart", f) },
|
|
5800
|
+
"install-service": {
|
|
5801
|
+
summary: "Install and start the background service",
|
|
5802
|
+
run: (f) => commandServiceAction("install", f)
|
|
5803
|
+
},
|
|
5804
|
+
"uninstall-service": {
|
|
5805
|
+
summary: "Stop and remove the background service",
|
|
5806
|
+
run: (f) => commandServiceAction("uninstall", f)
|
|
5807
|
+
},
|
|
5808
|
+
"service-status": {
|
|
5809
|
+
summary: "Show the background service state (same view as status)",
|
|
5810
|
+
run: commandStatus
|
|
5811
|
+
},
|
|
5812
|
+
logout: { summary: "Clear saved credentials and remove the service", run: commandLogout },
|
|
5813
|
+
run: { summary: "Run using saved credentials (used by the service)", run: commandRun },
|
|
5814
|
+
"start-service": { summary: "", run: (f) => commandServiceAction("start", f), hidden: true },
|
|
5815
|
+
"stop-service": { summary: "", run: (f) => commandServiceAction("stop", f), hidden: true },
|
|
5816
|
+
"restart-service": { summary: "", run: (f) => commandServiceAction("restart", f), hidden: true },
|
|
5817
|
+
"sign-out": { summary: "", run: commandLogout, hidden: true },
|
|
5818
|
+
unpair: { summary: "", run: commandLogout, hidden: true }
|
|
5819
|
+
};
|
|
5820
|
+
var OPTIONS = [
|
|
5821
|
+
["--api-url <url>", "Relay API URL"],
|
|
5822
|
+
["--token <token> --tunnel-id <id>", "Skip device auth and use an explicit credential"],
|
|
5823
|
+
["--reauth", "With connect: discard the saved credential and pair again"],
|
|
5824
|
+
["--daemon / --foreground", "With connect: skip the prompt and choose the mode"],
|
|
5825
|
+
["--json", "With status: machine-readable output"],
|
|
5826
|
+
["--keep-service", "With logout: keep the background service installed"]
|
|
5827
|
+
];
|
|
5486
5828
|
function showHelp() {
|
|
5487
|
-
|
|
5488
|
-
console.log(` ${
|
|
5489
|
-
console.log(` ${
|
|
5490
|
-
|
|
5491
|
-
console.log(` ${
|
|
5492
|
-
|
|
5493
|
-
console.log(` ${
|
|
5494
|
-
|
|
5495
|
-
console.log(`${
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
console.log(
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
console.log(
|
|
5509
|
-
console.log(` ${
|
|
5510
|
-
|
|
5511
|
-
console.log(` ${c2.white}--api-url${c2.reset} ${c2.dim}<url>${c2.reset} API URL ${c2.dim}(default: http://localhost:8080)${c2.reset}`);
|
|
5512
|
-
console.log(` ${c2.white}--daemon${c2.reset} With connect: skip the prompt and install the background service`);
|
|
5513
|
-
console.log(` ${c2.white}--foreground${c2.reset} With connect: skip prompts and run only in this terminal`);
|
|
5514
|
-
console.log("");
|
|
5515
|
-
console.log(` ${c2.dim}Config: ~/.agent-tunnel/config.json${c2.reset}`);
|
|
5516
|
-
console.log(` ${c2.dim}powered by ${c2.cyan}kortix${c2.reset}`);
|
|
5517
|
-
console.log("");
|
|
5829
|
+
blankLine();
|
|
5830
|
+
console.log(` ${c.cyan}▄▀█ █▀▀ █▀▀ █▄ █ ▀█▀${c.reset} ${c.cyan}▀█▀ █ █ █▄ █ █▄ █ █▀▀ █ ${c.reset}`);
|
|
5831
|
+
console.log(` ${c.cyan}█▀█ █▄█ ██▄ █ ▀█ █${c.reset} ${c.cyan} █ █▄█ █ ▀█ █ ▀█ ██▄ █▄▄${c.reset}`);
|
|
5832
|
+
blankLine();
|
|
5833
|
+
console.log(` ${c.dim}Secure bridge between AI agents & local machines${c.reset}`);
|
|
5834
|
+
blankLine();
|
|
5835
|
+
console.log(` ${c.bold}Usage${c.reset} ${c.dim}npx --yes @kortix/agent-tunnel@latest <command> [options]${c.reset}`);
|
|
5836
|
+
blankLine();
|
|
5837
|
+
console.log(`${c.gray} ── Commands ────────────────────────────────────────${c.reset}`);
|
|
5838
|
+
const visible = Object.entries(COMMANDS).filter(([, command]) => !command.hidden);
|
|
5839
|
+
const width = Math.max(...visible.map(([name]) => name.length)) + 2;
|
|
5840
|
+
for (const [name, command] of visible) {
|
|
5841
|
+
console.log(` ${c.cyan}${name.padEnd(width)}${c.reset}${command.summary}`);
|
|
5842
|
+
}
|
|
5843
|
+
blankLine();
|
|
5844
|
+
console.log(`${c.gray} ── Options ─────────────────────────────────────────${c.reset}`);
|
|
5845
|
+
const optionWidth = Math.max(...OPTIONS.map(([flag]) => flag.length)) + 2;
|
|
5846
|
+
for (const [flag, description] of OPTIONS) {
|
|
5847
|
+
console.log(` ${c.white}${flag.padEnd(optionWidth)}${c.reset}${c.dim}${description}${c.reset}`);
|
|
5848
|
+
}
|
|
5849
|
+
blankLine();
|
|
5850
|
+
console.log(` ${c.dim}Config: ${CONFIG_FILE2}${c.reset}`);
|
|
5851
|
+
console.log(` ${c.dim}powered by ${c.cyan}kortix${c.reset}`);
|
|
5852
|
+
blankLine();
|
|
5518
5853
|
}
|
|
5519
5854
|
var { command, flags } = parseArgs(process.argv);
|
|
5520
5855
|
if (Object.prototype.hasOwnProperty.call(flags, "keep-awake")) {
|
|
5521
|
-
console.error(
|
|
5856
|
+
console.error(` ${glyph.bad} --keep-awake is not supported. Configure sleep behavior in the operating system.`);
|
|
5522
5857
|
process.exit(2);
|
|
5523
5858
|
}
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
commandInstallService(flags);
|
|
5533
|
-
break;
|
|
5534
|
-
case "start":
|
|
5535
|
-
case "start-service":
|
|
5536
|
-
commandStartService();
|
|
5537
|
-
break;
|
|
5538
|
-
case "stop":
|
|
5539
|
-
case "stop-service":
|
|
5540
|
-
case "disable":
|
|
5541
|
-
commandStopService();
|
|
5542
|
-
break;
|
|
5543
|
-
case "restart":
|
|
5544
|
-
case "restart-service":
|
|
5545
|
-
commandRestartService();
|
|
5546
|
-
break;
|
|
5547
|
-
case "service-status":
|
|
5548
|
-
commandServiceStatus();
|
|
5549
|
-
break;
|
|
5550
|
-
case "logs":
|
|
5551
|
-
commandLogs();
|
|
5552
|
-
break;
|
|
5553
|
-
case "uninstall-service":
|
|
5554
|
-
commandUninstallService();
|
|
5555
|
-
break;
|
|
5556
|
-
case "status":
|
|
5557
|
-
commandStatus(flags);
|
|
5558
|
-
break;
|
|
5559
|
-
case "help":
|
|
5560
|
-
default:
|
|
5561
|
-
showHelp();
|
|
5562
|
-
break;
|
|
5859
|
+
var resolved = COMMANDS[command] ?? Object.values(COMMANDS).find((entry) => entry.aliases?.includes(command));
|
|
5860
|
+
if (!resolved) {
|
|
5861
|
+
showHelp();
|
|
5862
|
+
} else {
|
|
5863
|
+
Promise.resolve(resolved.run(flags)).catch((error) => {
|
|
5864
|
+
console.error(` ${glyph.bad} ${error instanceof Error ? error.message : String(error)}`);
|
|
5865
|
+
process.exit(1);
|
|
5866
|
+
});
|
|
5563
5867
|
}
|