@beeuni/agent-tunnel 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +74 -14
  2. package/dist/agent-cli.js +464 -249
  3. package/package.json +1 -12
package/dist/agent-cli.js CHANGED
@@ -2966,13 +2966,13 @@ if (typeof globalThis.WebSocket === "undefined") {
2966
2966
  }
2967
2967
 
2968
2968
  // src/agent/cli.ts
2969
- import { existsSync as existsSync7, readFileSync as readFileSync11, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
2969
+ import { existsSync as existsSync7, readFileSync as readFileSync11, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "fs";
2970
2970
  import { hostname as hostname4 } from "os";
2971
2971
  import { join as join12 } from "path";
2972
2972
 
2973
2973
  // src/agent/agent.ts
2974
2974
  import { spawn as spawn3 } from "child_process";
2975
- import { hostname as hostname2, platform as platform2, arch, release } from "os";
2975
+ import { hostname as hostname2, platform as platform3, arch, release } from "os";
2976
2976
 
2977
2977
  // src/agent/config.ts
2978
2978
  import { chmodSync, existsSync, lstatSync, readFileSync as readFileSync2 } from "fs";
@@ -2991,7 +2991,9 @@ var TunnelErrorCode = {
2991
2991
  AUTH_FAILED: -32007,
2992
2992
  ACCESS_PENDING: -32010,
2993
2993
  ACCESS_DENIED: -32011,
2994
- ACCESS_OFF: -32012
2994
+ ACCESS_OFF: -32012,
2995
+ DESKTOP_PERMISSION_MISSING: -32013,
2996
+ ACCESS_UNCONFIRMED: -32014
2995
2997
  };
2996
2998
  var TunnelMethods = {
2997
2999
  "fs.read": "filesystem",
@@ -3007,47 +3009,12 @@ var TunnelMethods = {
3007
3009
  "desktop.cua.list_tools": "desktop",
3008
3010
  "desktop.cua.describe": "desktop",
3009
3011
  "desktop.cua.call": "desktop",
3010
- "desktop.cua.bring_to_front": "desktop",
3011
- "desktop.cua.check_for_update": "desktop",
3012
- "desktop.cua.check_permissions": "desktop",
3013
- "desktop.cua.click": "desktop",
3014
- "desktop.cua.double_click": "desktop",
3015
- "desktop.cua.drag": "desktop",
3016
- "desktop.cua.end_session": "desktop",
3017
- "desktop.cua.get_accessibility_tree": "desktop",
3018
- "desktop.cua.get_agent_cursor_state": "desktop",
3019
- "desktop.cua.get_config": "desktop",
3020
- "desktop.cua.get_cursor_position": "desktop",
3021
- "desktop.cua.get_recording_state": "desktop",
3022
- "desktop.cua.get_screen_size": "desktop",
3023
- "desktop.cua.get_window_state": "desktop",
3024
- "desktop.cua.hotkey": "desktop",
3025
- "desktop.cua.kill_app": "desktop",
3026
- "desktop.cua.launch_app": "desktop",
3027
- "desktop.cua.list_apps": "desktop",
3028
- "desktop.cua.list_windows": "desktop",
3029
- "desktop.cua.move_cursor": "desktop",
3030
- "desktop.cua.page": "desktop",
3031
- "desktop.cua.press_key": "desktop",
3032
- "desktop.cua.replay_trajectory": "desktop",
3033
- "desktop.cua.right_click": "desktop",
3034
- "desktop.cua.scroll": "desktop",
3035
- "desktop.cua.set_agent_cursor_enabled": "desktop",
3036
- "desktop.cua.set_agent_cursor_motion": "desktop",
3037
- "desktop.cua.set_agent_cursor_style": "desktop",
3038
- "desktop.cua.set_config": "desktop",
3039
- "desktop.cua.set_value": "desktop",
3040
- "desktop.cua.start_recording": "desktop",
3041
- "desktop.cua.install_ffmpeg": "desktop",
3042
- "desktop.cua.start_session": "desktop",
3043
- "desktop.cua.stop_recording": "desktop",
3044
- "desktop.cua.type_text": "desktop",
3045
- "desktop.cua.zoom": "desktop",
3046
3012
  "tunnel.ping": null,
3047
3013
  "tunnel.pong": null,
3048
3014
  "tunnel.permission.revoked": null,
3049
3015
  "tunnel.permissions.sync": null,
3050
- "tunnel.token.rotated": null
3016
+ "tunnel.token.rotated": null,
3017
+ "tunnel.access.set": null
3051
3018
  };
3052
3019
 
3053
3020
  // src/shared/permissions.ts
@@ -3063,53 +3030,25 @@ var VALID_DESKTOP_FEATURES = new Set([
3063
3030
  "accessibility",
3064
3031
  "computer_use"
3065
3032
  ]);
3066
- var DESKTOP_METHOD_FEATURES = {
3067
- "desktop.cua.ensure": "computer_use",
3068
- "desktop.cua.start_daemon": "computer_use",
3069
- "desktop.cua.status": "computer_use",
3070
- "desktop.cua.version": "computer_use",
3071
- "desktop.cua.list_tools": "computer_use",
3072
- "desktop.cua.describe": "computer_use",
3073
- "desktop.cua.bring_to_front": "windows",
3074
- "desktop.cua.check_for_update": "computer_use",
3075
- "desktop.cua.check_permissions": "computer_use",
3076
- "desktop.cua.click": "mouse",
3077
- "desktop.cua.double_click": "mouse",
3078
- "desktop.cua.drag": "mouse",
3079
- "desktop.cua.end_session": "computer_use",
3080
- "desktop.cua.get_accessibility_tree": "accessibility",
3081
- "desktop.cua.get_agent_cursor_state": "mouse",
3082
- "desktop.cua.get_config": "computer_use",
3083
- "desktop.cua.get_cursor_position": "mouse",
3084
- "desktop.cua.get_recording_state": "computer_use",
3085
- "desktop.cua.get_screen_size": "screenshot",
3086
- "desktop.cua.get_window_state": "accessibility",
3087
- "desktop.cua.hotkey": "keyboard",
3088
- "desktop.cua.kill_app": "apps",
3089
- "desktop.cua.launch_app": "apps",
3090
- "desktop.cua.list_apps": "apps",
3091
- "desktop.cua.list_windows": "windows",
3092
- "desktop.cua.move_cursor": "mouse",
3093
- "desktop.cua.page": "accessibility",
3094
- "desktop.cua.press_key": "keyboard",
3095
- "desktop.cua.replay_trajectory": "computer_use",
3096
- "desktop.cua.right_click": "mouse",
3097
- "desktop.cua.scroll": "keyboard",
3098
- "desktop.cua.set_agent_cursor_enabled": "mouse",
3099
- "desktop.cua.set_agent_cursor_motion": "mouse",
3100
- "desktop.cua.set_agent_cursor_style": "mouse",
3101
- "desktop.cua.set_config": "computer_use",
3102
- "desktop.cua.set_value": "accessibility",
3103
- "desktop.cua.start_recording": "screenshot",
3104
- "desktop.cua.install_ffmpeg": "computer_use",
3105
- "desktop.cua.start_session": "computer_use",
3106
- "desktop.cua.stop_recording": "screenshot",
3107
- "desktop.cua.type_text": "keyboard",
3108
- "desktop.cua.zoom": "screenshot"
3109
- };
3033
+ var CUA_PREFIX = "desktop.cua.";
3110
3034
  function capabilityForMethod(method) {
3111
3035
  const capability = TunnelMethods[method];
3112
- return capability ?? null;
3036
+ if (capability !== undefined)
3037
+ return capability;
3038
+ return method.startsWith(CUA_PREFIX) && method.length > CUA_PREFIX.length ? "desktop" : null;
3039
+ }
3040
+ function normalizeDesktopCall(method, params) {
3041
+ if (!method.startsWith(CUA_PREFIX) || method in TunnelMethods)
3042
+ return { method, params };
3043
+ const { permissionId, ...args } = params;
3044
+ return {
3045
+ method: "desktop.cua.call",
3046
+ params: {
3047
+ tool: method.slice(CUA_PREFIX.length),
3048
+ args,
3049
+ ...permissionId === undefined ? {} : { permissionId }
3050
+ }
3051
+ };
3113
3052
  }
3114
3053
  function isTunnelCapability(value) {
3115
3054
  return VALID_CAPABILITIES.has(value);
@@ -3271,16 +3210,6 @@ function operationForMethod(method) {
3271
3210
  const separator = method.indexOf(".");
3272
3211
  return separator === -1 ? method : method.slice(separator + 1);
3273
3212
  }
3274
- function desktopFeatureForMethod(method, args = {}) {
3275
- if (method === "desktop.cua.call") {
3276
- const tool = args.tool;
3277
- if (typeof tool !== "string" || tool.length === 0)
3278
- return;
3279
- const toolMethod = tool.startsWith("desktop.cua.") ? tool : `desktop.cua.${tool}`;
3280
- return DESKTOP_METHOD_FEATURES[toolMethod];
3281
- }
3282
- return DESKTOP_METHOD_FEATURES[method];
3283
- }
3284
3213
 
3285
3214
  // src/agent/service-paths.ts
3286
3215
  import { createHash } from "crypto";
@@ -3288,7 +3217,6 @@ import { readFileSync, writeFileSync } from "fs";
3288
3217
  import { homedir } from "os";
3289
3218
  import { join, resolve } from "path";
3290
3219
  var SERVICE_LABEL = "ai.beeuni.agent-tunnel";
3291
- var DEFAULT_INSTALL_BACKGROUND_SERVICE = true;
3292
3220
  function defaultAgentTunnelHome() {
3293
3221
  return join(homedir(), ".agent-tunnel");
3294
3222
  }
@@ -3543,10 +3471,13 @@ function loadConfig(overrides = {}) {
3543
3471
  import { spawn } from "child_process";
3544
3472
  import { randomUUID } from "crypto";
3545
3473
  import { chmodSync as chmodSync2, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, renameSync, rmSync, statSync, writeFileSync as writeFileSync2 } from "fs";
3474
+ import { platform } from "os";
3546
3475
  import { isAbsolute, join as join3 } from "path";
3547
3476
  var ACCESS_HOLD_MS = 20000;
3548
3477
  var DESKTOP_APP_HEARTBEAT_MS = 30000;
3549
3478
  var MAX_GRANT_MS = 24 * 3600000;
3479
+ var DENY_MS = 10 * 60000;
3480
+ var REQUEST_FRESH_MS = 60000;
3550
3481
  var MODES = ["ask", "always", "off"];
3551
3482
  var COMPAT = { mode: "always", grantedUntil: null, deniedUntil: null, keepAwake: false };
3552
3483
  var accessFilePath = (home = agentTunnelHome()) => join3(home, "access.json");
@@ -3596,6 +3527,99 @@ function decideAccess(state, now = Date.now()) {
3596
3527
  const granted = state.grantedUntil ? Date.parse(state.grantedUntil) : 0;
3597
3528
  return granted > now && granted - now <= MAX_GRANT_MS ? "run" : "ask";
3598
3529
  }
3530
+ var keepAwakeSupported = (os) => os === "darwin" || os === "linux";
3531
+ function parseAccessChange(params) {
3532
+ const raw = params && typeof params === "object" ? params : {};
3533
+ const change = {};
3534
+ if (raw.mode !== undefined) {
3535
+ if (!MODES.includes(raw.mode))
3536
+ throw new Error(`mode must be one of ${MODES.join(", ")}`);
3537
+ change.mode = raw.mode;
3538
+ }
3539
+ if (raw.grantMinutes !== undefined) {
3540
+ if (typeof raw.grantMinutes !== "number" || !(raw.grantMinutes > 0))
3541
+ throw new Error("grantMinutes must be a positive number");
3542
+ change.grantMinutes = raw.grantMinutes;
3543
+ }
3544
+ for (const key of ["revoke", "deny", "keepAwake"]) {
3545
+ if (raw[key] === undefined)
3546
+ continue;
3547
+ if (typeof raw[key] !== "boolean")
3548
+ throw new Error(`${key} must be true or false`);
3549
+ change[key] = raw[key];
3550
+ }
3551
+ if (Object.keys(change).length === 0)
3552
+ throw new Error("No access change given");
3553
+ return change;
3554
+ }
3555
+ function accessCommand(change) {
3556
+ const npx = "npx @beeuni/agent-tunnel@latest";
3557
+ if (change.keepAwake !== undefined && Object.keys(change).length === 1)
3558
+ return `${npx} keep-awake ${change.keepAwake ? "on" : "off"}`;
3559
+ const args = [change.mode, change.grantMinutes !== undefined ? `--allow-for ${change.grantMinutes}` : undefined];
3560
+ return [`${npx} access`, ...args.filter(Boolean)].join(" ");
3561
+ }
3562
+ function nextAccess(current, change, now = Date.now()) {
3563
+ const next = { ...current };
3564
+ if (change.mode !== undefined) {
3565
+ if (!MODES.includes(change.mode))
3566
+ throw new Error(`mode must be one of ${MODES.join(", ")}`);
3567
+ if (change.mode !== current.mode) {
3568
+ next.grantedUntil = null;
3569
+ next.deniedUntil = null;
3570
+ }
3571
+ next.mode = change.mode;
3572
+ }
3573
+ if (change.grantMinutes !== undefined) {
3574
+ const minutes = Number(change.grantMinutes);
3575
+ if (!Number.isFinite(minutes) || minutes <= 0)
3576
+ throw new Error("grantMinutes must be a positive number");
3577
+ next.grantedUntil = new Date(now + Math.min(minutes * 60000, MAX_GRANT_MS)).toISOString();
3578
+ next.deniedUntil = null;
3579
+ }
3580
+ if (change.revoke === true)
3581
+ next.grantedUntil = null;
3582
+ if (change.deny === true) {
3583
+ next.grantedUntil = null;
3584
+ next.deniedUntil = new Date(now + DENY_MS).toISOString();
3585
+ }
3586
+ if (change.keepAwake !== undefined)
3587
+ next.keepAwake = change.keepAwake === true;
3588
+ return next;
3589
+ }
3590
+ function accessWidens(current, next, now = Date.now()) {
3591
+ if (next.mode === "always")
3592
+ return current.mode !== "always";
3593
+ if (next.mode !== "ask")
3594
+ return false;
3595
+ const reach = (state) => state.mode === "ask" && decideAccess(state, now) === "run" ? Date.parse(state.grantedUntil) : 0;
3596
+ return reach(next) > reach(current);
3597
+ }
3598
+ function pendingAccessRequest(home = agentTunnelHome(), now = Date.now()) {
3599
+ const request = readAccessRequest(home);
3600
+ return request && now - Date.parse(request.requestedAt) < REQUEST_FRESH_MS ? request : null;
3601
+ }
3602
+ function accessView(home = agentTunnelHome(), os = platform(), now = Date.now()) {
3603
+ const state = readAccess(home);
3604
+ const request = pendingAccessRequest(home, now);
3605
+ return {
3606
+ ...state,
3607
+ grantedUntil: state.grantedUntil && Date.parse(state.grantedUntil) > now ? state.grantedUntil : null,
3608
+ deniedUntil: state.deniedUntil && Date.parse(state.deniedUntil) > now ? state.deniedUntil : null,
3609
+ keepAwakeSupported: keepAwakeSupported(os),
3610
+ pendingRequest: request ? { id: request.id, capability: request.capability, requestedAt: request.requestedAt, ...request.change ? { change: request.change } : {} } : null
3611
+ };
3612
+ }
3613
+ function applyAccess(change, home = agentTunnelHome(), now = Date.now()) {
3614
+ const request = readAccessRequest(home);
3615
+ const declinesChange = change.deny === true && Boolean(request?.change);
3616
+ const own = declinesChange ? { ...change, deny: undefined } : change;
3617
+ writeAccess(nextAccess(readAccess(home), own, now), home);
3618
+ const answers = own.mode !== undefined || own.grantMinutes !== undefined || change.deny === true;
3619
+ if (request && answers && !(Date.parse(request.requestedAt) > now))
3620
+ rmSync(accessRequestPath(home), { force: true });
3621
+ return accessView(home, platform(), now);
3622
+ }
3599
3623
  function writeAccessRequest(input, home = agentTunnelHome()) {
3600
3624
  const request = { id: randomUUID(), requestedAt: new Date().toISOString(), ...input };
3601
3625
  writePrivateJson(accessRequestPath(home), home, request);
@@ -3653,10 +3677,10 @@ function wakeDesktopApp(home = agentTunnelHome(), now = Date.now()) {
3653
3677
  return "no-app";
3654
3678
  }
3655
3679
  }
3656
- function keepAwakeCommand(platform, pid) {
3657
- if (platform === "darwin")
3680
+ function keepAwakeCommand(platform2, pid) {
3681
+ if (platform2 === "darwin")
3658
3682
  return { command: "caffeinate", args: ["-s", "-w", String(pid)] };
3659
- if (platform === "linux") {
3683
+ if (platform2 === "linux") {
3660
3684
  return {
3661
3685
  command: "systemd-inhibit",
3662
3686
  args: ["--what=sleep", "--who=Beeuni", "--why=Keep this computer reachable", "--mode=block", "tail", `--pid=${pid}`, "-f", "/dev/null"]
@@ -3669,7 +3693,7 @@ function keepAwakeCommand(platform, pid) {
3669
3693
  import { execFileSync, spawn as spawn2 } from "child_process";
3670
3694
  import { createHash as createHash2 } from "crypto";
3671
3695
  import { readFileSync as readFileSync4 } from "fs";
3672
- import { hostname, platform } from "os";
3696
+ import { hostname, platform as platform2 } from "os";
3673
3697
  var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3674
3698
  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;
3675
3699
  var SETUP_TOKEN_PATTERN = /^beeuni_tnl_[A-Za-z0-9_-]{32,64}$/;
@@ -3776,7 +3800,7 @@ function parseDeviceAuthStatus(value) {
3776
3800
  }
3777
3801
  }
3778
3802
  function machineDisplayName({
3779
- os = platform(),
3803
+ os = platform2(),
3780
3804
  host = hostname(),
3781
3805
  env = process.env,
3782
3806
  run = (command, args) => execFileSync(command, args, { encoding: "utf8", timeout: 1000, stdio: ["ignore", "pipe", "ignore"] })
@@ -3790,7 +3814,7 @@ function machineDisplayName({
3790
3814
  }
3791
3815
  }
3792
3816
  function machineId({
3793
- os = platform(),
3817
+ os = platform2(),
3794
3818
  run = (command, args) => execFileSync(command, args, { encoding: "utf8", timeout: 2000, stdio: ["ignore", "pipe", "ignore"] }),
3795
3819
  read = (path) => readFileSync4(path, "utf8")
3796
3820
  } = {}) {
@@ -3859,7 +3883,7 @@ function openBrowser(url) {
3859
3883
  darwin: ["open", [safeUrl]],
3860
3884
  win32: ["rundll32.exe", ["url.dll,FileProtocolHandler", safeUrl]]
3861
3885
  };
3862
- const [command, args] = opener[platform()] ?? ["xdg-open", [safeUrl]];
3886
+ const [command, args] = opener[platform2()] ?? ["xdg-open", [safeUrl]];
3863
3887
  try {
3864
3888
  spawn2(command, args, { detached: true, stdio: "ignore" }).unref();
3865
3889
  } catch {}
@@ -4012,6 +4036,10 @@ function verifyMessageSignature(signingKey, payload, nonce, signature) {
4012
4036
 
4013
4037
  // src/agent/agent.ts
4014
4038
  var AGENT_VERSION = agentTunnelVersion();
4039
+ function openRelaySocket(url) {
4040
+ const RelaySocket = WebSocket;
4041
+ return new RelaySocket(url, { headers: { "User-Agent": `beeuni-agent-tunnel/${AGENT_VERSION}` } });
4042
+ }
4015
4043
  var AUTH_REJECTED_CLOSE_CODES = [4001, 4003];
4016
4044
  var RELAY_FAULT_4001_REASONS = new Set(["auth timeout", "authentication error", "authentication response failed"]);
4017
4045
  function isCredentialRejection(code, reason = "") {
@@ -4029,6 +4057,7 @@ function log(icon, msg) {
4029
4057
  }
4030
4058
  var LIVENESS_TIMEOUT_MS = 75000;
4031
4059
  var WATCHDOG_INTERVAL_MS = 5000;
4060
+ var DESKTOP_PROBE_EVERY_TICKS = 12;
4032
4061
  var CLOCK_JUMP_MS = 30000;
4033
4062
  var REJECTED_RETRY_MS = 5 * 60000;
4034
4063
  var PAIR_AGAIN_COMMAND = "npx @beeuni/agent-tunnel@latest connect --reauth";
@@ -4077,6 +4106,9 @@ class TunnelAgent {
4077
4106
  standbyAttempts = 0;
4078
4107
  lastRejectedLogAt = 0;
4079
4108
  sentAccessKey = null;
4109
+ desktopPermissions = null;
4110
+ probingDesktop = false;
4111
+ watchdogTicks = 0;
4080
4112
  keepAwake = null;
4081
4113
  keepAwakeFailed = false;
4082
4114
  reconnectTimer = null;
@@ -4109,7 +4141,7 @@ class TunnelAgent {
4109
4141
  this.connectStartedAt = this.now();
4110
4142
  this.setStatus("connecting");
4111
4143
  try {
4112
- this.ws = new WebSocket(new URL(wsUrl));
4144
+ this.ws = openRelaySocket(new URL(wsUrl).href);
4113
4145
  this.setupWsHandlers();
4114
4146
  } catch (err) {
4115
4147
  log(`${c.red}✗${c.reset}`, `Connection failed`);
@@ -4168,7 +4200,8 @@ class TunnelAgent {
4168
4200
  token: trustedCredential(this.config.token, "token"),
4169
4201
  capabilities: this.registry.getCapabilityNames(),
4170
4202
  agentVersion: AGENT_VERSION,
4171
- reportsAccess: true
4203
+ reportsAccess: true,
4204
+ controlsAccess: true
4172
4205
  });
4173
4206
  });
4174
4207
  this.ws.addEventListener("message", (event) => {
@@ -4262,6 +4295,7 @@ class TunnelAgent {
4262
4295
  this.standbyAttempts = 0;
4263
4296
  this.setStatus("online");
4264
4297
  this.reportAccess();
4298
+ this.probeDesktopPermissions();
4265
4299
  if (this.stableConnectionTimer)
4266
4300
  clearTimeout(this.stableConnectionTimer);
4267
4301
  this.stableConnectionTimer = setTimeout(() => {
@@ -4311,6 +4345,10 @@ class TunnelAgent {
4311
4345
  log(`${c.yellow}!${c.reset}`, `Token rotated — reconnecting with new token`);
4312
4346
  return;
4313
4347
  }
4348
+ if ("id" in msg && msg.id && msg.method === "tunnel.access.set") {
4349
+ await this.handleAccessChange(msg);
4350
+ return;
4351
+ }
4314
4352
  if ("id" in msg && msg.id) {
4315
4353
  await this.handleRpcRequest(msg);
4316
4354
  return;
@@ -4337,7 +4375,8 @@ class TunnelAgent {
4337
4375
  return true;
4338
4376
  }
4339
4377
  async handleRpcRequest(request) {
4340
- const { id, method, params = {} } = request;
4378
+ const { method, params } = normalizeDesktopCall(request.method, request.params ?? {});
4379
+ const { id } = request;
4341
4380
  const permissionId = params.permissionId;
4342
4381
  const permission = this.permissionGuard.getPermissionForMethod(permissionId, method);
4343
4382
  if (!permission) {
@@ -4362,7 +4401,8 @@ class TunnelAgent {
4362
4401
  this.sendSignedResult(id, result);
4363
4402
  } catch (err) {
4364
4403
  const message = err instanceof Error ? err.message : String(err);
4365
- this.sendSignedError(id, -32003, message);
4404
+ const code = err?.code;
4405
+ this.sendSignedError(id, typeof code === "number" && code <= -32000 && code >= -32099 ? code : -32003, message);
4366
4406
  }
4367
4407
  }
4368
4408
  sendSignedResult(id, result) {
@@ -4415,7 +4455,7 @@ class TunnelAgent {
4415
4455
  hostname: hostname2(),
4416
4456
  displayName: this.displayName ??= machineDisplayName(),
4417
4457
  ...this.hardwareId ? { machineId: this.hardwareId } : {},
4418
- platform: platform2(),
4458
+ platform: platform3(),
4419
4459
  arch: arch(),
4420
4460
  osVersion: release(),
4421
4461
  agentVersion: AGENT_VERSION,
@@ -4436,10 +4476,11 @@ class TunnelAgent {
4436
4476
  try {
4437
4477
  const pending = readAccessRequest(this.home);
4438
4478
  if (pending && Date.now() - Date.parse(pending.requestedAt) < hold) {
4439
- requestId = pending.id;
4479
+ requestId = pending.change ? null : pending.id;
4440
4480
  } else {
4441
4481
  requestId = writeAccessRequest({ capability: capabilityForMethod(method) ?? method, method }, this.home).id;
4442
4482
  wakeDesktopApp(this.home);
4483
+ this.reportAccess();
4443
4484
  }
4444
4485
  } catch {}
4445
4486
  const deadline = Date.now() + hold;
@@ -4454,15 +4495,83 @@ class TunnelAgent {
4454
4495
  }
4455
4496
  return ACCESS_ERRORS.pending;
4456
4497
  }
4498
+ async handleAccessChange(request) {
4499
+ let change;
4500
+ try {
4501
+ change = parseAccessChange(request.params);
4502
+ } catch (err) {
4503
+ this.sendSignedError(request.id, TunnelErrorCode.LOCAL_ERROR, err instanceof Error ? err.message : String(err));
4504
+ return;
4505
+ }
4506
+ try {
4507
+ const view = await this.changeAccess(change);
4508
+ this.reportAccess();
4509
+ this.sendSignedResult(request.id, view);
4510
+ } catch (err) {
4511
+ this.reportAccess();
4512
+ this.sendSignedError(request.id, TunnelErrorCode.ACCESS_UNCONFIRMED, err instanceof Error ? err.message : String(err));
4513
+ }
4514
+ }
4515
+ async changeAccess(change) {
4516
+ const current = readAccess(this.home);
4517
+ const target = nextAccess(current, change, this.now());
4518
+ if (!accessWidens(current, target, this.now()))
4519
+ return applyAccess(change, this.home, this.now());
4520
+ const unconfirmed = (why) => new Error(`computer_access_unconfirmed: ${why}`);
4521
+ const asked = writeAccessRequest({ capability: "access", method: "tunnel.access.set", change }, this.home);
4522
+ this.reportAccess();
4523
+ if (wakeDesktopApp(this.home) === "no-app") {
4524
+ clearAccessRequest(asked.id, this.home);
4525
+ throw unconfirmed(`Only the owner can allow more access, on the computer itself. Run ${accessCommand(change)} there.`);
4526
+ }
4527
+ const hold = this.options.accessHoldMs ?? ACCESS_HOLD_MS;
4528
+ const deadline = Date.now() + hold;
4529
+ while (Date.now() < deadline) {
4530
+ await new Promise((resolve2) => setTimeout(resolve2, Math.min(100, hold / 4)));
4531
+ if (!accessWidens(readAccess(this.home), target, this.now())) {
4532
+ clearAccessRequest(asked.id, this.home);
4533
+ return accessView(this.home, platform3(), this.now());
4534
+ }
4535
+ if (readAccessRequest(this.home)?.id !== asked.id)
4536
+ throw unconfirmed("The owner declined on the computer.");
4537
+ }
4538
+ clearAccessRequest(asked.id, this.home);
4539
+ throw unconfirmed(`Nobody confirmed on the computer within ${Math.round(hold / 1000)} seconds.`);
4540
+ }
4457
4541
  reportAccess() {
4458
- const access = readAccess(this.home);
4459
- const { mode } = access;
4460
- const grantedUntil = access.grantedUntil && Date.parse(access.grantedUntil) > this.now() ? access.grantedUntil : null;
4461
- const key = `${mode}|${grantedUntil}`;
4462
- if (key === this.sentAccessKey || this.status !== "online")
4542
+ if (this.status !== "online")
4543
+ return;
4544
+ const view = accessView(this.home, platform3(), this.now());
4545
+ const params = {
4546
+ mode: view.mode,
4547
+ grantedUntil: view.grantedUntil,
4548
+ deniedUntil: view.deniedUntil,
4549
+ keepAwake: view.keepAwake,
4550
+ keepAwakeSupported: view.keepAwakeSupported,
4551
+ pendingRequest: view.pendingRequest ? { capability: view.pendingRequest.capability, requestedAt: view.pendingRequest.requestedAt, ...view.pendingRequest.change ? { change: view.pendingRequest.change } : {} } : null,
4552
+ desktopPermissions: this.desktopPermissions
4553
+ };
4554
+ const key = JSON.stringify(params);
4555
+ if (key === this.sentAccessKey)
4463
4556
  return;
4464
4557
  this.sentAccessKey = key;
4465
- this.sendSigned({ jsonrpc: "2.0", method: "tunnel.access.state", params: { mode, grantedUntil } });
4558
+ this.sendSigned({ jsonrpc: "2.0", method: "tunnel.access.state", params });
4559
+ }
4560
+ async probeDesktopPermissions() {
4561
+ const call = this.registry.getHandler("desktop.cua.call");
4562
+ if (!call || this.probingDesktop)
4563
+ return;
4564
+ this.probingDesktop = true;
4565
+ try {
4566
+ const report = await call({ tool: "check_permissions", args: {}, __permission: { capability: "desktop" } });
4567
+ this.desktopPermissions = {
4568
+ accessibility: report?.accessibility === true,
4569
+ screenRecording: report?.screen_recording === true
4570
+ };
4571
+ } catch {} finally {
4572
+ this.probingDesktop = false;
4573
+ }
4574
+ this.reportAccess();
4466
4575
  }
4467
4576
  syncKeepAwake(want) {
4468
4577
  if (!want) {
@@ -4473,7 +4582,7 @@ class TunnelAgent {
4473
4582
  }
4474
4583
  if (this.keepAwake || this.keepAwakeFailed)
4475
4584
  return;
4476
- const blocker = keepAwakeCommand(platform2(), process.pid);
4585
+ const blocker = keepAwakeCommand(platform3(), process.pid);
4477
4586
  if (!blocker)
4478
4587
  return;
4479
4588
  const child = spawn3(blocker.command, blocker.args, { stdio: "ignore" });
@@ -4517,6 +4626,8 @@ class TunnelAgent {
4517
4626
  this.syncKeepAwake(access.keepAwake);
4518
4627
  this.reportAccess();
4519
4628
  } catch {}
4629
+ if (++this.watchdogTicks % DESKTOP_PROBE_EVERY_TICKS === 0 && this.status === "online")
4630
+ this.probeDesktopPermissions();
4520
4631
  }, interval);
4521
4632
  this.watchdog.unref?.();
4522
4633
  }
@@ -4582,7 +4693,7 @@ class TunnelAgent {
4582
4693
  }
4583
4694
 
4584
4695
  // src/agent/banner.ts
4585
- import { arch as arch2, hostname as hostname3, platform as platform3 } from "os";
4696
+ import { arch as arch2, hostname as hostname3, platform as platform4 } from "os";
4586
4697
  var BOX_WIDTH = 60;
4587
4698
  var BAR_WIDTH = 50;
4588
4699
  var BAR_FRAMES = 14;
@@ -4632,7 +4743,7 @@ async function printStartupBanner({
4632
4743
  blank();
4633
4744
  row(` ${c.dim}tunnel${c.reset} ${c.white}${truncate(tunnelId, 40)}${c.reset}`);
4634
4745
  row(` ${c.dim}relay${c.reset} ${c.white}${truncate(apiUrl, 40)}${c.reset}`);
4635
- row(` ${c.dim}machine${c.reset} ${c.white}${truncate(hostname3(), 28)}${c.reset} ${c.dim}(${platform3()} ${arch2()})${c.reset}`);
4746
+ row(` ${c.dim}machine${c.reset} ${c.white}${truncate(hostname3(), 28)}${c.reset} ${c.dim}(${platform4()} ${arch2()})${c.reset}`);
4636
4747
  row(` ${c.dim}access${c.reset} ${capabilityRow}`);
4637
4748
  blank();
4638
4749
  console.log(` ${c.gray}╰${"─".repeat(BOX_WIDTH - brand.length - 3)} ${c.dim}created by ${c.cyan}beeuni${c.reset} ${c.gray}─╯${c.reset}`);
@@ -4666,8 +4777,8 @@ class CapabilityRegistry {
4666
4777
 
4667
4778
  // src/agent/capabilities/desktop/cua-driver.ts
4668
4779
  import { spawn as spawn4 } from "child_process";
4669
- import { existsSync as existsSync3, readFileSync as readFileSync6, realpathSync, statSync as statSync2 } from "fs";
4670
- import { homedir as homedir3, platform as platform4, tmpdir } from "os";
4780
+ import { existsSync as existsSync3, readFileSync as readFileSync6, realpathSync, rmSync as rmSync2, statSync as statSync2 } from "fs";
4781
+ import { homedir as homedir3, platform as platform5, tmpdir } from "os";
4671
4782
  import { basename as basename2, dirname as dirname3, join as join5 } from "path";
4672
4783
  var MAX_DRIVER_OUTPUT_BYTES = 5 * 1024 * 1024;
4673
4784
  var DRIVER_ENV_KEYS = [
@@ -4785,7 +4896,7 @@ function execFile(cmd, args, timeoutMs = 30000, env = driverEnvironment()) {
4785
4896
  clearTimeout(timer);
4786
4897
  if (code !== 0) {
4787
4898
  const detail = stderr.trim() || stdout.trim();
4788
- reject(new Error(`${cmd} failed (${code})${detail ? `: ${detail}` : ""}`));
4899
+ reject(new Error(`${basename2(cmd)} failed (${code})${detail ? `: ${detail}` : ""}`));
4789
4900
  } else {
4790
4901
  resolve2({ stdout, stderr });
4791
4902
  }
@@ -4833,7 +4944,8 @@ class CuaDriver {
4833
4944
  this.binary = options.binary ?? null;
4834
4945
  this.embedded = options.embedded;
4835
4946
  const uid = typeof process.getuid === "function" ? process.getuid() : "user";
4836
- this.socketPath = options.socketPath ?? join5(tmpdir(), `beeuni-cua-${uid}.sock`);
4947
+ const home = serviceLabelFor(agentTunnelHome()).slice(SERVICE_LABEL.length);
4948
+ this.socketPath = options.socketPath ?? join5(tmpdir(), `beeuni-cua-${uid}${home}.sock`);
4837
4949
  this.hostBundleId = options.hostBundleId;
4838
4950
  }
4839
4951
  isEmbedded(binary) {
@@ -4913,7 +5025,9 @@ class CuaDriver {
4913
5025
  const bin = await this.ensureInstalled();
4914
5026
  if (this.isEmbedded(bin)) {
4915
5027
  await this.startEmbeddedDaemon(bin);
4916
- } else if (platform4() === "darwin") {
5028
+ this.daemonReady = true;
5029
+ return { ok: true };
5030
+ } else if (platform5() === "darwin") {
4917
5031
  const child = spawn4("open", ["-n", "-g", "-a", "CuaDriver", "--args", "serve"], {
4918
5032
  detached: true,
4919
5033
  stdio: "ignore",
@@ -4942,6 +5056,12 @@ class CuaDriver {
4942
5056
  if (this.daemon && this.daemon.exitCode === null)
4943
5057
  return;
4944
5058
  const { env, socket } = this.mode(bin);
5059
+ if (existsSync3(this.socketPath)) {
5060
+ await execFile(bin, ["stop", ...socket], 1e4, env).catch(() => {
5061
+ return;
5062
+ });
5063
+ rmSync2(this.socketPath, { force: true });
5064
+ }
4945
5065
  const child = spawn4(bin, ["serve", "--embedded", "--no-permissions-gate", ...socket], {
4946
5066
  stdio: "ignore",
4947
5067
  env
@@ -4965,12 +5085,32 @@ class CuaDriver {
4965
5085
  throw new Error("cua-driver did not start (no socket after 10 s)");
4966
5086
  }
4967
5087
  }
5088
+ async permissionOwner() {
5089
+ return this.isEmbedded(await this.ensureInstalled()) ? "Beeuni" : "CuaDriver";
5090
+ }
5091
+ async missingPermissions() {
5092
+ const report = await this.call("check_permissions", {});
5093
+ const missing = [];
5094
+ if (report?.accessibility === false)
5095
+ missing.push("Accessibility");
5096
+ if (report?.screen_recording === false)
5097
+ missing.push("Screen Recording");
5098
+ return missing;
5099
+ }
4968
5100
  stop() {
4969
5101
  this.daemon?.kill();
4970
5102
  this.daemon = null;
4971
5103
  this.daemonReady = false;
4972
5104
  }
4973
5105
  async ensureDaemonReady() {
5106
+ const bin = await this.ensureInstalled();
5107
+ if (this.isEmbedded(bin)) {
5108
+ if (this.daemon?.exitCode === null && existsSync3(this.socketPath))
5109
+ return;
5110
+ this.stop();
5111
+ await this.startDaemon();
5112
+ return;
5113
+ }
4974
5114
  if (this.daemonReady)
4975
5115
  return;
4976
5116
  try {
@@ -4987,44 +5127,6 @@ class CuaDriver {
4987
5127
  }
4988
5128
 
4989
5129
  // src/agent/capabilities/desktop.ts
4990
- var CUA_TOOLS = [
4991
- "bring_to_front",
4992
- "check_for_update",
4993
- "check_permissions",
4994
- "click",
4995
- "double_click",
4996
- "drag",
4997
- "end_session",
4998
- "get_accessibility_tree",
4999
- "get_agent_cursor_state",
5000
- "get_config",
5001
- "get_cursor_position",
5002
- "get_recording_state",
5003
- "get_screen_size",
5004
- "get_window_state",
5005
- "hotkey",
5006
- "kill_app",
5007
- "launch_app",
5008
- "list_apps",
5009
- "list_windows",
5010
- "move_cursor",
5011
- "page",
5012
- "press_key",
5013
- "replay_trajectory",
5014
- "right_click",
5015
- "scroll",
5016
- "set_agent_cursor_enabled",
5017
- "set_agent_cursor_motion",
5018
- "set_agent_cursor_style",
5019
- "set_config",
5020
- "set_value",
5021
- "start_recording",
5022
- "install_ffmpeg",
5023
- "start_session",
5024
- "stop_recording",
5025
- "type_text",
5026
- "zoom"
5027
- ];
5028
5130
  var LOCAL_ONLY_CUA_TOOLS = new Set(["check_for_update", "install_ffmpeg"]);
5029
5131
  function assertRemotelyCallableTool(tool) {
5030
5132
  if (typeof tool !== "string" || tool.length === 0) {
@@ -5034,24 +5136,30 @@ function assertRemotelyCallableTool(tool) {
5034
5136
  throw new Error(`CUA tool "${tool}" is local-only and cannot run through Agent Tunnel`);
5035
5137
  }
5036
5138
  }
5037
- function createDesktopCapability() {
5139
+
5140
+ class DesktopPermissionMissingError extends Error {
5141
+ code = TunnelErrorCode.DESKTOP_PERMISSION_MISSING;
5142
+ }
5143
+ async function explainFailure(cua, err, onMissing) {
5144
+ const missing = await cua.missingPermissions().catch(() => []);
5145
+ if (missing.length > 0) {
5146
+ onMissing();
5147
+ throw new DesktopPermissionMissingError(`computer_desktop_permission_missing: macOS has not given ${await cua.permissionOwner()} ${missing.join(" and ")} on this computer.`);
5148
+ }
5149
+ throw err;
5150
+ }
5151
+ function createDesktopCapability(options = {}) {
5038
5152
  const cua = new CuaDriver;
5039
5153
  const methods = new Map;
5040
- const assertDesktopPermission = (params, method) => {
5154
+ const onMissing = options.onPermissionMissing ?? (() => {});
5155
+ const assertDesktopPermission = (params) => {
5041
5156
  const permission = params.__permission;
5042
5157
  if (permission?.capability !== "desktop") {
5043
5158
  throw new Error("Permission denied: desktop permission required");
5044
5159
  }
5045
- const features = Array.isArray(permission.scope?.features) ? permission.scope.features.filter((value) => typeof value === "string") : [];
5046
- if (features.length === 0)
5047
- return;
5048
- const feature = desktopFeatureForMethod(method, params);
5049
- if (!feature || !features.includes(feature)) {
5050
- throw new Error(`Permission denied: desktop feature "${feature ?? "unknown"}" is not allowed`);
5051
- }
5052
5160
  };
5053
5161
  methods.set("desktop.cua.ensure", async (params) => {
5054
- assertDesktopPermission(params, "desktop.cua.ensure");
5162
+ assertDesktopPermission(params);
5055
5163
  const binary = await cua.ensureInstalled();
5056
5164
  const version = await cua.version().catch(() => {
5057
5165
  return;
@@ -5059,40 +5167,36 @@ function createDesktopCapability() {
5059
5167
  return { ok: true, binary, version };
5060
5168
  });
5061
5169
  methods.set("desktop.cua.start_daemon", async (params) => {
5062
- assertDesktopPermission(params, "desktop.cua.start_daemon");
5170
+ assertDesktopPermission(params);
5063
5171
  return cua.startDaemon();
5064
5172
  });
5065
5173
  methods.set("desktop.cua.status", async (params) => {
5066
- assertDesktopPermission(params, "desktop.cua.status");
5174
+ assertDesktopPermission(params);
5067
5175
  return { status: await cua.status() };
5068
5176
  });
5069
5177
  methods.set("desktop.cua.version", async (params) => {
5070
- assertDesktopPermission(params, "desktop.cua.version");
5178
+ assertDesktopPermission(params);
5071
5179
  return { version: await cua.version() };
5072
5180
  });
5073
5181
  methods.set("desktop.cua.list_tools", async (params) => {
5074
- assertDesktopPermission(params, "desktop.cua.list_tools");
5182
+ assertDesktopPermission(params);
5075
5183
  return { tools: await cua.listTools() };
5076
5184
  });
5077
5185
  methods.set("desktop.cua.describe", async (params) => {
5078
- assertDesktopPermission(params, "desktop.cua.describe");
5186
+ assertDesktopPermission(params);
5079
5187
  return { description: await cua.describe(params.tool) };
5080
5188
  });
5081
5189
  methods.set("desktop.cua.call", async (params) => {
5082
- assertDesktopPermission(params, "desktop.cua.call");
5190
+ assertDesktopPermission(params);
5083
5191
  const tool = params.tool;
5084
5192
  assertRemotelyCallableTool(tool);
5085
5193
  const args = params.args || {};
5086
- return cua.call(tool, args);
5194
+ try {
5195
+ return await cua.call(tool, args);
5196
+ } catch (err) {
5197
+ return explainFailure(cua, err, onMissing);
5198
+ }
5087
5199
  });
5088
- for (const tool of CUA_TOOLS) {
5089
- if (LOCAL_ONLY_CUA_TOOLS.has(tool))
5090
- continue;
5091
- methods.set(`desktop.cua.${tool}`, async (params) => {
5092
- assertDesktopPermission(params, `desktop.cua.${tool}`);
5093
- return cua.call(tool, params);
5094
- });
5095
- }
5096
5200
  return {
5097
5201
  name: "desktop",
5098
5202
  methods
@@ -5503,7 +5607,7 @@ function createShellCapability(config) {
5503
5607
  }
5504
5608
 
5505
5609
  // src/agent/capabilities/enabled-registry.ts
5506
- function createEnabledCapabilityRegistry(config, findDesktopDriver = findCuaDriverBinary) {
5610
+ function createEnabledCapabilityRegistry(config, findDesktopDriver = findCuaDriverBinary, desktop = {}) {
5507
5611
  const registry = new CapabilityRegistry;
5508
5612
  const enabled = new Set(config.enabledCapabilities ?? ["filesystem", "shell", "desktop"]);
5509
5613
  if (enabled.has("filesystem"))
@@ -5511,13 +5615,13 @@ function createEnabledCapabilityRegistry(config, findDesktopDriver = findCuaDriv
5511
5615
  if (enabled.has("shell"))
5512
5616
  registry.register(createShellCapability(config));
5513
5617
  if (enabled.has("desktop") && findDesktopDriver()) {
5514
- registry.register(createDesktopCapability());
5618
+ registry.register(createDesktopCapability(desktop));
5515
5619
  }
5516
5620
  return registry;
5517
5621
  }
5518
5622
 
5519
5623
  // src/agent/credential-store.ts
5520
- import { chmodSync as chmodSync3, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync7, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
5624
+ import { chmodSync as chmodSync3, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync7, renameSync as renameSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
5521
5625
  import { join as join8 } from "path";
5522
5626
  var CONFIG_DIR2 = agentTunnelHome();
5523
5627
  var CONFIG_FILE2 = join8(CONFIG_DIR2, "config.json");
@@ -5561,7 +5665,7 @@ function clearSavedCredentials() {
5561
5665
  try {
5562
5666
  existing = JSON.parse(readFileSync7(CONFIG_FILE2, "utf-8"));
5563
5667
  } catch {
5564
- rmSync2(CONFIG_FILE2, { force: true });
5668
+ rmSync3(CONFIG_FILE2, { force: true });
5565
5669
  return true;
5566
5670
  }
5567
5671
  const hadCredentials = PAIRING_FIELDS.some((key) => (key in existing) && existing[key] != null);
@@ -5577,17 +5681,17 @@ function probeCredentials(config, options = {}) {
5577
5681
  return new Promise((resolve3) => {
5578
5682
  let socket;
5579
5683
  try {
5580
- socket = new WebSocket(new URL(buildTunnelWsUrl(config)));
5684
+ socket = openRelaySocket(new URL(buildTunnelWsUrl(config)).href);
5581
5685
  } catch {
5582
5686
  resolve3("unreachable");
5583
5687
  return;
5584
5688
  }
5585
5689
  const settle = (result) => {
5586
5690
  clearTimeout(timer);
5691
+ resolve3(result);
5587
5692
  try {
5588
5693
  socket.close(1000, "probe complete");
5589
5694
  } catch {}
5590
- resolve3(result);
5591
5695
  };
5592
5696
  const timer = setTimeout(() => settle("unreachable"), options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS);
5593
5697
  socket.addEventListener("open", () => {
@@ -5642,7 +5746,6 @@ function isShellStartupNoise(line) {
5642
5746
  }
5643
5747
 
5644
5748
  // src/agent/prompts.ts
5645
- import { createInterface } from "readline/promises";
5646
5749
  function isInteractiveTerminal() {
5647
5750
  return process.stdin.isTTY === true && process.stdout.isTTY === true;
5648
5751
  }
@@ -5652,41 +5755,16 @@ function isTruthyFlag(value) {
5652
5755
  function anyFlag(flags, names) {
5653
5756
  return names.some((name) => isTruthyFlag(flags[name]));
5654
5757
  }
5655
- async function promptYesNo(question, defaultValue) {
5656
- const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
5657
- const rl = createInterface({ input: process.stdin, output: process.stdout });
5658
- try {
5659
- for (;; ) {
5660
- const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase();
5661
- if (!answer)
5662
- return defaultValue;
5663
- if (["y", "yes"].includes(answer))
5664
- return true;
5665
- if (["n", "no"].includes(answer))
5666
- return false;
5667
- console.log(` ${c.yellow}!${c.reset} Please answer yes or no.`);
5668
- }
5669
- } catch (error) {
5670
- if (error instanceof Error && error.name === "AbortError") {
5671
- process.stdout.write(`
5672
- `);
5673
- process.exit(130);
5674
- }
5675
- throw error;
5676
- } finally {
5677
- rl.close();
5678
- }
5679
- }
5680
5758
 
5681
5759
  // src/agent/service.ts
5682
- import { chmodSync as chmodSync4, copyFileSync, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync9, realpathSync as realpathSync3, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "fs";
5683
- import { platform as platform6 } from "os";
5760
+ import { chmodSync as chmodSync4, copyFileSync, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync9, realpathSync as realpathSync3, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "fs";
5761
+ import { homedir as homedir5, platform as platform7 } from "os";
5684
5762
  import { join as join10 } from "path";
5685
5763
 
5686
5764
  // src/agent/service-drivers.ts
5687
5765
  import { spawnSync } from "child_process";
5688
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync8, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
5689
- import { homedir as homedir4, platform as platform5, userInfo } from "os";
5766
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync8, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
5767
+ import { homedir as homedir4, platform as platform6, userInfo } from "os";
5690
5768
  import { dirname as dirname6, join as join9 } from "path";
5691
5769
 
5692
5770
  // src/agent/service-quoting.ts
@@ -5835,7 +5913,7 @@ var launchd = {
5835
5913
  const existed = existsSync5(paths.launchdPlist);
5836
5914
  const stop = run("launchctl", ["bootout", launchdTarget(), paths.launchdPlist]);
5837
5915
  if (existed)
5838
- rmSync3(paths.launchdPlist, { force: true });
5916
+ rmSync4(paths.launchdPlist, { force: true });
5839
5917
  return { detail: stop.detail };
5840
5918
  },
5841
5919
  start(paths, installed) {
@@ -5888,7 +5966,7 @@ var systemd = {
5888
5966
  const existed = existsSync5(paths.systemdUnit);
5889
5967
  const disable = run("systemctl", ["--user", "disable", "--now", `${paths.label}.service`]);
5890
5968
  if (existed)
5891
- rmSync3(paths.systemdUnit, { force: true });
5969
+ rmSync4(paths.systemdUnit, { force: true });
5892
5970
  run("systemctl", ["--user", "daemon-reload"]);
5893
5971
  return { detail: disable.detail };
5894
5972
  },
@@ -5963,8 +6041,8 @@ var scheduledTask = {
5963
6041
  const stop = run("schtasks.exe", ["/End", "/TN", paths.label]);
5964
6042
  const del = run("schtasks.exe", ["/Delete", "/TN", paths.label, "/F"]);
5965
6043
  if (existed)
5966
- rmSync3(paths.windowsScript, { force: true });
5967
- rmSync3(windowsTaskXmlPath(paths), { force: true });
6044
+ rmSync4(paths.windowsScript, { force: true });
6045
+ rmSync4(windowsTaskXmlPath(paths), { force: true });
5968
6046
  return { detail: joinDetails(stop, del) };
5969
6047
  },
5970
6048
  start(paths, installed) {
@@ -6010,7 +6088,7 @@ function serviceDriverFor(os) {
6010
6088
  return DRIVERS[os];
6011
6089
  }
6012
6090
  function serviceDriver() {
6013
- return serviceDriverFor(platform5());
6091
+ return serviceDriverFor(platform6());
6014
6092
  }
6015
6093
 
6016
6094
  // src/agent/service.ts
@@ -6046,8 +6124,22 @@ function runnerPartsFor(script, runtime = {
6046
6124
  }
6047
6125
  return { command: runtime.execPath, args: [vendor(script, paths), "run", "--service"], env };
6048
6126
  }
6127
+ function installedAppRunner(apps = ["/Applications/Beeuni.app", join10(homedir5(), "Applications", "Beeuni.app")], os = process.platform) {
6128
+ if (os !== "darwin")
6129
+ return null;
6130
+ for (const app of apps) {
6131
+ const execPath = join10(app, "Contents", "MacOS", "Beeuni");
6132
+ const script = join10(app, "Contents", "Resources", "agent-tunnel", "agent-cli.js");
6133
+ if (existsSync6(execPath) && existsSync6(script))
6134
+ return { execPath, script };
6135
+ }
6136
+ return null;
6137
+ }
6049
6138
  var plannedRunner = (script, paths) => isEphemeralRunnerPath(script) ? paths.vendoredRunner : script;
6050
6139
  function currentRunnerParts(vendor = vendorRunner) {
6140
+ const app = process.versions.electron ? null : installedAppRunner();
6141
+ if (app)
6142
+ return runnerPartsFor(app.script, { execPath: app.execPath, electron: "installed-app" }, getServicePaths(), vendor);
6051
6143
  const script = process.argv[1];
6052
6144
  if (script && existsSync6(script))
6053
6145
  return runnerPartsFor(script, undefined, getServicePaths(), vendor);
@@ -6062,7 +6154,7 @@ function withDriver(operate, fallback) {
6062
6154
  const installed = existsSync6(path);
6063
6155
  const outcome = operate(driver, paths, installed);
6064
6156
  return {
6065
- platform: platform6(),
6157
+ platform: platform7(),
6066
6158
  installed: outcome.installed ?? installed,
6067
6159
  active: outcome.active ?? fallback.active,
6068
6160
  ...outcome.enabled !== undefined ? { enabled: outcome.enabled } : {},
@@ -6080,7 +6172,7 @@ function installService() {
6080
6172
  return withDriver((driver, servicePaths) => driver.install(servicePaths, runner), { installed: false, active: null });
6081
6173
  }
6082
6174
  function uninstallService() {
6083
- rmSync4(getServicePaths().binDir, { recursive: true, force: true });
6175
+ rmSync5(getServicePaths().binDir, { recursive: true, force: true });
6084
6176
  return withDriver((driver, paths) => ({ ...driver.uninstall(paths), installed: false, active: false }), { installed: false, active: false });
6085
6177
  }
6086
6178
  function startService() {
@@ -6108,7 +6200,7 @@ function restartService() {
6108
6200
  function getServiceStatus() {
6109
6201
  if (!serviceDriver()) {
6110
6202
  return {
6111
- platform: platform6(),
6203
+ platform: platform7(),
6112
6204
  installed: false,
6113
6205
  active: null,
6114
6206
  detail: SUPPORTED_PLATFORMS_MESSAGE
@@ -6165,6 +6257,35 @@ function renderServiceAction(action, status) {
6165
6257
  blankLine();
6166
6258
  }
6167
6259
 
6260
+ // src/agent/self-restart.ts
6261
+ import { statSync as statSync3 } from "fs";
6262
+ function runtimeFingerprint(files) {
6263
+ return files.map((file) => {
6264
+ try {
6265
+ const stat2 = statSync3(file);
6266
+ return `${stat2.ino}:${stat2.mtimeMs}:${stat2.size}`;
6267
+ } catch {
6268
+ return "missing";
6269
+ }
6270
+ }).join("|");
6271
+ }
6272
+ var PERMISSION_RESTART_MIN_UPTIME_S = 5 * 60;
6273
+ function watchForRestart(exit, intervalMs = 60000) {
6274
+ const files = [process.execPath, process.argv[1]].filter((file) => Boolean(file));
6275
+ const started = runtimeFingerprint(files);
6276
+ setInterval(() => {
6277
+ if (runtimeFingerprint(files) !== started)
6278
+ exit("its code was updated");
6279
+ }, intervalMs).unref();
6280
+ return {
6281
+ permissionMissing: () => {
6282
+ if (process.uptime() < PERMISSION_RESTART_MIN_UPTIME_S)
6283
+ return;
6284
+ setTimeout(() => exit("macOS refused a desktop permission"), 1000).unref();
6285
+ }
6286
+ };
6287
+ }
6288
+
6168
6289
  // src/agent/state-file.ts
6169
6290
  import { chmodSync as chmodSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync10, renameSync as renameSync3, writeFileSync as writeFileSync6 } from "fs";
6170
6291
  import { join as join11 } from "path";
@@ -6191,10 +6312,22 @@ function writeAgentState(status, tunnel, home = agentTunnelHome()) {
6191
6312
  renameSync3(tmp, stateFilePath(home));
6192
6313
  } catch {}
6193
6314
  }
6315
+ function isAlive2(pid) {
6316
+ if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0)
6317
+ return false;
6318
+ try {
6319
+ process.kill(pid, 0);
6320
+ return true;
6321
+ } catch (error) {
6322
+ return error.code === "EPERM";
6323
+ }
6324
+ }
6194
6325
  function readAgentState(home = agentTunnelHome()) {
6195
6326
  try {
6196
6327
  const parsed = JSON.parse(readFileSync10(stateFilePath(home), "utf8"));
6197
- return parsed && typeof parsed === "object" && typeof parsed.status === "string" ? parsed : null;
6328
+ if (!parsed || typeof parsed !== "object" || typeof parsed.status !== "string")
6329
+ return null;
6330
+ return parsed.status !== "offline" && !isAlive2(parsed.pid) ? { ...parsed, status: "offline" } : parsed;
6198
6331
  } catch {
6199
6332
  return null;
6200
6333
  }
@@ -6207,13 +6340,18 @@ var FOREGROUND_FLAGS = ["foreground", "no-daemon", "no-service", "no-background"
6207
6340
  var DEFAULT_LOG_LINES = 60;
6208
6341
  function parseArgs(argv) {
6209
6342
  const flags = {};
6343
+ const words = [];
6210
6344
  for (let i = 3;i < argv.length; i++) {
6211
6345
  const arg = argv[i];
6212
- if (!arg.startsWith("--"))
6346
+ if (!arg.startsWith("--")) {
6347
+ words.push(arg);
6213
6348
  continue;
6349
+ }
6214
6350
  const next = argv[i + 1];
6215
6351
  flags[arg.slice(2)] = next && !next.startsWith("--") ? argv[++i] : "true";
6216
6352
  }
6353
+ if (words.length > 0)
6354
+ flags._ = words.join(" ");
6217
6355
  return { command: argv[2] || "help", flags };
6218
6356
  }
6219
6357
  var jsonMode = false;
@@ -6238,7 +6376,16 @@ function startAgent(config, options = {}) {
6238
6376
  process.stdout.write = process.stderr.write.bind(process.stderr);
6239
6377
  options = { ...options, service: true };
6240
6378
  }
6241
- const registry = createEnabledCapabilityRegistry(config);
6379
+ let agent = null;
6380
+ const restart = options.service ? watchForRestart((reason) => {
6381
+ process.stdout.write(`[agent-tunnel] restarting: ${reason}
6382
+ `);
6383
+ agent?.disconnect();
6384
+ process.exit(0);
6385
+ }) : null;
6386
+ const registry = createEnabledCapabilityRegistry(config, undefined, {
6387
+ onPermissionMissing: restart?.permissionMissing
6388
+ });
6242
6389
  if (config.enabledCapabilities?.includes("desktop") && !registry.has("desktop")) {
6243
6390
  console.error("[agent-tunnel] Computer Use is approved but unavailable: install the trusted cua-driver locally, then restart Agent Tunnel.");
6244
6391
  }
@@ -6254,7 +6401,7 @@ function startAgent(config, options = {}) {
6254
6401
  version: agentTunnelVersion()
6255
6402
  });
6256
6403
  }
6257
- const agent = new TunnelAgent(config, registry, { onStatus: (status, current) => writeAgentState(status, current) }, {
6404
+ agent = new TunnelAgent(config, registry, { onStatus: (status, current) => writeAgentState(status, current) }, {
6258
6405
  persistent: options.service === true,
6259
6406
  reloadConfig: () => loadConfig({ apiUrl: config.apiUrl })
6260
6407
  });
@@ -6263,31 +6410,37 @@ function startAgent(config, options = {}) {
6263
6410
  if (!options.service)
6264
6411
  console.log(`
6265
6412
  ${c.dim} Shutting down…${c.reset}`);
6266
- agent.disconnect();
6413
+ agent?.disconnect();
6267
6414
  process.exit(0);
6268
6415
  };
6269
6416
  process.on("SIGTERM", shutdown);
6270
6417
  process.on("SIGINT", shutdown);
6271
6418
  }
6272
- async function chooseBackgroundMode(flags) {
6419
+ function chooseBackgroundMode(flags) {
6273
6420
  if (anyFlag(flags, BACKGROUND_FLAGS))
6274
6421
  return true;
6275
6422
  if (anyFlag(flags, FOREGROUND_FLAGS))
6276
6423
  return false;
6277
- if (jsonMode || !isInteractiveTerminal())
6278
- return false;
6424
+ return !jsonMode && isInteractiveTerminal();
6425
+ }
6426
+ function printServiceControls() {
6427
+ const npx = "npx @beeuni/agent-tunnel@latest";
6428
+ console.log(` ${c.dim}Runs in the background: starts at login, restarts after failures, keeps running after this terminal closes.${c.reset}`);
6429
+ if (installedAppRunner()) {
6430
+ console.log(` ${c.dim}It runs on the Beeuni app on this Mac: same connection, same macOS permissions. Control it from Your computer in the app.${c.reset}`);
6431
+ }
6279
6432
  blankLine();
6280
- console.log(` ${glyph.warn} ${c.bold}Security note${c.reset}`);
6281
- console.log(` ${c.dim}Background mode starts at login, continues after this terminal closes, and restarts after failures.${c.reset}`);
6282
- console.log(` ${c.dim}The computer must remain powered on, awake, and connected to the internet.${c.reset}`);
6433
+ field("Pause", `${c.white}${npx} stop${c.reset}`);
6434
+ field("Resume", `${c.white}${npx} start${c.reset}`);
6435
+ field("Disconnect", `${c.white}${npx} logout${c.reset}`);
6283
6436
  blankLine();
6284
- return promptYesNo(" Install the background service now?", DEFAULT_INSTALL_BACKGROUND_SERVICE);
6285
6437
  }
6286
6438
  async function launch(config, flags, lease) {
6287
- if (await chooseBackgroundMode(flags)) {
6439
+ if (chooseBackgroundMode(flags)) {
6288
6440
  saveCredentials(config.tunnelId, config.token, config.apiUrl);
6289
6441
  if (!jsonMode) {
6290
6442
  renderServiceAction("install", SERVICE_ACTIONS.install.run());
6443
+ printServiceControls();
6291
6444
  return;
6292
6445
  }
6293
6446
  try {
@@ -6495,7 +6648,7 @@ function commandStatus(flags) {
6495
6648
  serviceLabel: getServicePaths().label,
6496
6649
  service,
6497
6650
  state: readAgentState(),
6498
- access: readAccess(),
6651
+ access: accessView(),
6499
6652
  lastActivity: lastServiceActivity()
6500
6653
  }, null, 2));
6501
6654
  return;
@@ -6552,8 +6705,8 @@ async function unpairOnServer() {
6552
6705
  async function commandLogout(flags) {
6553
6706
  const serverUnpaired = await unpairOnServer();
6554
6707
  const removed = clearSavedCredentials();
6555
- rmSync5(accessFilePath(), { force: true });
6556
- rmSync5(accessRequestPath(), { force: true });
6708
+ rmSync6(accessFilePath(), { force: true });
6709
+ rmSync6(accessRequestPath(), { force: true });
6557
6710
  const keepService = isTruthyFlag(flags["keep-service"]);
6558
6711
  if (!keepService)
6559
6712
  SERVICE_ACTIONS.uninstall.run();
@@ -6624,12 +6777,74 @@ function commandServiceAction(action, flags) {
6624
6777
  }
6625
6778
  renderServiceAction(action, SERVICE_ACTIONS[action].run());
6626
6779
  }
6780
+ function describeAccess(view) {
6781
+ const clock = (iso) => new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
6782
+ if (view.mode === "always")
6783
+ return `${c.white}Always allowed${c.reset}`;
6784
+ if (view.mode === "off")
6785
+ return `${c.white}Off${c.reset} ${c.dim}(every agent call is refused)${c.reset}`;
6786
+ const now = view.grantedUntil ? `allowed until ${clock(view.grantedUntil)}` : view.deniedUntil ? `denied until ${clock(view.deniedUntil)}` : "asks before the next call";
6787
+ return `${c.white}Ask each time${c.reset} ${c.dim}· ${now}${c.reset}`;
6788
+ }
6789
+ function printAccess(view, flags) {
6790
+ if (isTruthyFlag(flags.json)) {
6791
+ console.log(JSON.stringify(view, null, 2));
6792
+ return;
6793
+ }
6794
+ const npx = "npx @beeuni/agent-tunnel@latest";
6795
+ blankLine();
6796
+ field("access", describeAccess(view));
6797
+ field("keep awake", view.keepAwakeSupported ? view.keepAwake ? `${c.white}on${c.reset}` : `${c.white}off${c.reset}` : `${c.dim}not available on this system${c.reset}`);
6798
+ if (view.pendingRequest) {
6799
+ field("waiting", `${c.white}${view.pendingRequest.change ? "a change asked for through Beeuni" : `an agent call (${view.pendingRequest.capability})`}${c.reset}`);
6800
+ field("", `${c.dim}${npx} access --allow-for 60 or --deny${c.reset}`);
6801
+ }
6802
+ blankLine();
6803
+ console.log(` ${c.dim}Change it:${c.reset} ${c.white}${npx} access ask|always|off${c.reset}${c.dim}, --allow-for <minutes>, --revoke, --deny${c.reset}`);
6804
+ blankLine();
6805
+ }
6806
+ function commandAccess(flags) {
6807
+ const change = {};
6808
+ if (flags._)
6809
+ change.mode = flags._;
6810
+ if (flags["allow-for"] !== undefined) {
6811
+ change.grantMinutes = Number(flags["allow-for"]);
6812
+ if (!(change.grantMinutes > 0))
6813
+ fail("--allow-for takes minutes, at most 1440. Example: agent-tunnel access --allow-for 60");
6814
+ }
6815
+ if (isTruthyFlag(flags.revoke))
6816
+ change.revoke = true;
6817
+ if (isTruthyFlag(flags.deny))
6818
+ change.deny = true;
6819
+ if (Object.keys(change).length === 0)
6820
+ return printAccess(accessView(), flags);
6821
+ if (change.mode !== undefined && !["ask", "always", "off"].includes(change.mode)) {
6822
+ fail("Access is one of: ask, always, off. Example: agent-tunnel access ask");
6823
+ }
6824
+ if (change.grantMinutes !== undefined && change.mode === undefined && readAccess().mode !== "ask")
6825
+ change.mode = "ask";
6826
+ printAccess(applyAccess(change), flags);
6827
+ }
6828
+ function commandKeepAwake(flags) {
6829
+ if (flags._ === undefined)
6830
+ return printAccess(accessView(), flags);
6831
+ if (flags._ !== "on" && flags._ !== "off")
6832
+ fail("Keep awake is on or off. Example: agent-tunnel keep-awake on");
6833
+ if (!accessView().keepAwakeSupported)
6834
+ fail("Keep awake is not available on this system yet.");
6835
+ printAccess(applyAccess({ keepAwake: flags._ === "on" }), flags);
6836
+ }
6627
6837
  var COMMANDS = {
6628
6838
  connect: {
6629
6839
  summary: "Pair this machine, then run it in the background or this terminal",
6630
6840
  run: commandConnect
6631
6841
  },
6632
6842
  status: { summary: "Show pairing, capabilities, and service state (--json)", run: commandStatus },
6843
+ access: {
6844
+ summary: "When Beeuni may use this computer: ask, always, off (--allow-for <min>, --revoke, --deny, --json)",
6845
+ run: commandAccess
6846
+ },
6847
+ "keep-awake": { summary: "Keep this computer awake while connected: on, off (--json)", run: commandKeepAwake },
6633
6848
  logs: { summary: "Show recent service logs (--lines <n>, --all, --clear)", run: commandLogs },
6634
6849
  start: {
6635
6850
  summary: "Start the background service (resumes a paused one)",
@@ -6668,7 +6883,7 @@ var OPTIONS = [
6668
6883
  ["--reauth", "With connect: discard the saved credential and pair again"],
6669
6884
  ["--daemon / --foreground", "With connect: skip the prompt and choose the mode"],
6670
6885
  ["--project-id <uuid>", 'With connect: offer "Also share with <project>" on the approval page; the computer is yours in every project either way'],
6671
- ["--json", "With connect: NDJSON events, no prompts. With status and logout: machine-readable output"],
6886
+ ["--json", "With connect: NDJSON events, no prompts. With status, access, keep-awake, and logout: machine-readable output"],
6672
6887
  ["--keep-service", "With logout: keep the background service installed"]
6673
6888
  ];
6674
6889
  function showHelp() {
@@ -6700,14 +6915,14 @@ function showHelp() {
6700
6915
  var { command, flags } = parseArgs(process.argv);
6701
6916
  jsonMode = command === "connect" && isTruthyFlag(flags.json);
6702
6917
  if (Object.prototype.hasOwnProperty.call(flags, "keep-awake")) {
6703
- console.error(` ${glyph.bad} --keep-awake is not supported. Configure sleep behavior in the operating system.`);
6918
+ console.error(` ${glyph.bad} --keep-awake is not a flag. Run: agent-tunnel keep-awake on`);
6704
6919
  process.exit(2);
6705
6920
  }
6706
6921
  var resolved = COMMANDS[command] ?? Object.values(COMMANDS).find((entry) => entry.aliases?.includes(command));
6707
6922
  if (!resolved) {
6708
6923
  showHelp();
6709
6924
  } else {
6710
- Promise.resolve(resolved.run(flags)).catch((error) => {
6925
+ Promise.resolve().then(() => resolved.run(flags)).catch((error) => {
6711
6926
  fail(error instanceof Error ? error.message : String(error));
6712
6927
  });
6713
6928
  }