@beeuni/agent-tunnel 0.1.0 → 0.1.1

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 +457 -246
  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 {}
@@ -4029,6 +4053,7 @@ function log(icon, msg) {
4029
4053
  }
4030
4054
  var LIVENESS_TIMEOUT_MS = 75000;
4031
4055
  var WATCHDOG_INTERVAL_MS = 5000;
4056
+ var DESKTOP_PROBE_EVERY_TICKS = 12;
4032
4057
  var CLOCK_JUMP_MS = 30000;
4033
4058
  var REJECTED_RETRY_MS = 5 * 60000;
4034
4059
  var PAIR_AGAIN_COMMAND = "npx @beeuni/agent-tunnel@latest connect --reauth";
@@ -4077,6 +4102,9 @@ class TunnelAgent {
4077
4102
  standbyAttempts = 0;
4078
4103
  lastRejectedLogAt = 0;
4079
4104
  sentAccessKey = null;
4105
+ desktopPermissions = null;
4106
+ probingDesktop = false;
4107
+ watchdogTicks = 0;
4080
4108
  keepAwake = null;
4081
4109
  keepAwakeFailed = false;
4082
4110
  reconnectTimer = null;
@@ -4168,7 +4196,8 @@ class TunnelAgent {
4168
4196
  token: trustedCredential(this.config.token, "token"),
4169
4197
  capabilities: this.registry.getCapabilityNames(),
4170
4198
  agentVersion: AGENT_VERSION,
4171
- reportsAccess: true
4199
+ reportsAccess: true,
4200
+ controlsAccess: true
4172
4201
  });
4173
4202
  });
4174
4203
  this.ws.addEventListener("message", (event) => {
@@ -4262,6 +4291,7 @@ class TunnelAgent {
4262
4291
  this.standbyAttempts = 0;
4263
4292
  this.setStatus("online");
4264
4293
  this.reportAccess();
4294
+ this.probeDesktopPermissions();
4265
4295
  if (this.stableConnectionTimer)
4266
4296
  clearTimeout(this.stableConnectionTimer);
4267
4297
  this.stableConnectionTimer = setTimeout(() => {
@@ -4311,6 +4341,10 @@ class TunnelAgent {
4311
4341
  log(`${c.yellow}!${c.reset}`, `Token rotated — reconnecting with new token`);
4312
4342
  return;
4313
4343
  }
4344
+ if ("id" in msg && msg.id && msg.method === "tunnel.access.set") {
4345
+ await this.handleAccessChange(msg);
4346
+ return;
4347
+ }
4314
4348
  if ("id" in msg && msg.id) {
4315
4349
  await this.handleRpcRequest(msg);
4316
4350
  return;
@@ -4337,7 +4371,8 @@ class TunnelAgent {
4337
4371
  return true;
4338
4372
  }
4339
4373
  async handleRpcRequest(request) {
4340
- const { id, method, params = {} } = request;
4374
+ const { method, params } = normalizeDesktopCall(request.method, request.params ?? {});
4375
+ const { id } = request;
4341
4376
  const permissionId = params.permissionId;
4342
4377
  const permission = this.permissionGuard.getPermissionForMethod(permissionId, method);
4343
4378
  if (!permission) {
@@ -4362,7 +4397,8 @@ class TunnelAgent {
4362
4397
  this.sendSignedResult(id, result);
4363
4398
  } catch (err) {
4364
4399
  const message = err instanceof Error ? err.message : String(err);
4365
- this.sendSignedError(id, -32003, message);
4400
+ const code = err?.code;
4401
+ this.sendSignedError(id, typeof code === "number" && code <= -32000 && code >= -32099 ? code : -32003, message);
4366
4402
  }
4367
4403
  }
4368
4404
  sendSignedResult(id, result) {
@@ -4415,7 +4451,7 @@ class TunnelAgent {
4415
4451
  hostname: hostname2(),
4416
4452
  displayName: this.displayName ??= machineDisplayName(),
4417
4453
  ...this.hardwareId ? { machineId: this.hardwareId } : {},
4418
- platform: platform2(),
4454
+ platform: platform3(),
4419
4455
  arch: arch(),
4420
4456
  osVersion: release(),
4421
4457
  agentVersion: AGENT_VERSION,
@@ -4436,10 +4472,11 @@ class TunnelAgent {
4436
4472
  try {
4437
4473
  const pending = readAccessRequest(this.home);
4438
4474
  if (pending && Date.now() - Date.parse(pending.requestedAt) < hold) {
4439
- requestId = pending.id;
4475
+ requestId = pending.change ? null : pending.id;
4440
4476
  } else {
4441
4477
  requestId = writeAccessRequest({ capability: capabilityForMethod(method) ?? method, method }, this.home).id;
4442
4478
  wakeDesktopApp(this.home);
4479
+ this.reportAccess();
4443
4480
  }
4444
4481
  } catch {}
4445
4482
  const deadline = Date.now() + hold;
@@ -4454,15 +4491,83 @@ class TunnelAgent {
4454
4491
  }
4455
4492
  return ACCESS_ERRORS.pending;
4456
4493
  }
4494
+ async handleAccessChange(request) {
4495
+ let change;
4496
+ try {
4497
+ change = parseAccessChange(request.params);
4498
+ } catch (err) {
4499
+ this.sendSignedError(request.id, TunnelErrorCode.LOCAL_ERROR, err instanceof Error ? err.message : String(err));
4500
+ return;
4501
+ }
4502
+ try {
4503
+ const view = await this.changeAccess(change);
4504
+ this.reportAccess();
4505
+ this.sendSignedResult(request.id, view);
4506
+ } catch (err) {
4507
+ this.reportAccess();
4508
+ this.sendSignedError(request.id, TunnelErrorCode.ACCESS_UNCONFIRMED, err instanceof Error ? err.message : String(err));
4509
+ }
4510
+ }
4511
+ async changeAccess(change) {
4512
+ const current = readAccess(this.home);
4513
+ const target = nextAccess(current, change, this.now());
4514
+ if (!accessWidens(current, target, this.now()))
4515
+ return applyAccess(change, this.home, this.now());
4516
+ const unconfirmed = (why) => new Error(`computer_access_unconfirmed: ${why}`);
4517
+ const asked = writeAccessRequest({ capability: "access", method: "tunnel.access.set", change }, this.home);
4518
+ this.reportAccess();
4519
+ if (wakeDesktopApp(this.home) === "no-app") {
4520
+ clearAccessRequest(asked.id, this.home);
4521
+ throw unconfirmed(`Only the owner can allow more access, on the computer itself. Run ${accessCommand(change)} there.`);
4522
+ }
4523
+ const hold = this.options.accessHoldMs ?? ACCESS_HOLD_MS;
4524
+ const deadline = Date.now() + hold;
4525
+ while (Date.now() < deadline) {
4526
+ await new Promise((resolve2) => setTimeout(resolve2, Math.min(100, hold / 4)));
4527
+ if (!accessWidens(readAccess(this.home), target, this.now())) {
4528
+ clearAccessRequest(asked.id, this.home);
4529
+ return accessView(this.home, platform3(), this.now());
4530
+ }
4531
+ if (readAccessRequest(this.home)?.id !== asked.id)
4532
+ throw unconfirmed("The owner declined on the computer.");
4533
+ }
4534
+ clearAccessRequest(asked.id, this.home);
4535
+ throw unconfirmed(`Nobody confirmed on the computer within ${Math.round(hold / 1000)} seconds.`);
4536
+ }
4457
4537
  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")
4538
+ if (this.status !== "online")
4539
+ return;
4540
+ const view = accessView(this.home, platform3(), this.now());
4541
+ const params = {
4542
+ mode: view.mode,
4543
+ grantedUntil: view.grantedUntil,
4544
+ deniedUntil: view.deniedUntil,
4545
+ keepAwake: view.keepAwake,
4546
+ keepAwakeSupported: view.keepAwakeSupported,
4547
+ pendingRequest: view.pendingRequest ? { capability: view.pendingRequest.capability, requestedAt: view.pendingRequest.requestedAt, ...view.pendingRequest.change ? { change: view.pendingRequest.change } : {} } : null,
4548
+ desktopPermissions: this.desktopPermissions
4549
+ };
4550
+ const key = JSON.stringify(params);
4551
+ if (key === this.sentAccessKey)
4463
4552
  return;
4464
4553
  this.sentAccessKey = key;
4465
- this.sendSigned({ jsonrpc: "2.0", method: "tunnel.access.state", params: { mode, grantedUntil } });
4554
+ this.sendSigned({ jsonrpc: "2.0", method: "tunnel.access.state", params });
4555
+ }
4556
+ async probeDesktopPermissions() {
4557
+ const call = this.registry.getHandler("desktop.cua.call");
4558
+ if (!call || this.probingDesktop)
4559
+ return;
4560
+ this.probingDesktop = true;
4561
+ try {
4562
+ const report = await call({ tool: "check_permissions", args: {}, __permission: { capability: "desktop" } });
4563
+ this.desktopPermissions = {
4564
+ accessibility: report?.accessibility === true,
4565
+ screenRecording: report?.screen_recording === true
4566
+ };
4567
+ } catch {} finally {
4568
+ this.probingDesktop = false;
4569
+ }
4570
+ this.reportAccess();
4466
4571
  }
4467
4572
  syncKeepAwake(want) {
4468
4573
  if (!want) {
@@ -4473,7 +4578,7 @@ class TunnelAgent {
4473
4578
  }
4474
4579
  if (this.keepAwake || this.keepAwakeFailed)
4475
4580
  return;
4476
- const blocker = keepAwakeCommand(platform2(), process.pid);
4581
+ const blocker = keepAwakeCommand(platform3(), process.pid);
4477
4582
  if (!blocker)
4478
4583
  return;
4479
4584
  const child = spawn3(blocker.command, blocker.args, { stdio: "ignore" });
@@ -4517,6 +4622,8 @@ class TunnelAgent {
4517
4622
  this.syncKeepAwake(access.keepAwake);
4518
4623
  this.reportAccess();
4519
4624
  } catch {}
4625
+ if (++this.watchdogTicks % DESKTOP_PROBE_EVERY_TICKS === 0 && this.status === "online")
4626
+ this.probeDesktopPermissions();
4520
4627
  }, interval);
4521
4628
  this.watchdog.unref?.();
4522
4629
  }
@@ -4582,7 +4689,7 @@ class TunnelAgent {
4582
4689
  }
4583
4690
 
4584
4691
  // src/agent/banner.ts
4585
- import { arch as arch2, hostname as hostname3, platform as platform3 } from "os";
4692
+ import { arch as arch2, hostname as hostname3, platform as platform4 } from "os";
4586
4693
  var BOX_WIDTH = 60;
4587
4694
  var BAR_WIDTH = 50;
4588
4695
  var BAR_FRAMES = 14;
@@ -4632,7 +4739,7 @@ async function printStartupBanner({
4632
4739
  blank();
4633
4740
  row(` ${c.dim}tunnel${c.reset} ${c.white}${truncate(tunnelId, 40)}${c.reset}`);
4634
4741
  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}`);
4742
+ row(` ${c.dim}machine${c.reset} ${c.white}${truncate(hostname3(), 28)}${c.reset} ${c.dim}(${platform4()} ${arch2()})${c.reset}`);
4636
4743
  row(` ${c.dim}access${c.reset} ${capabilityRow}`);
4637
4744
  blank();
4638
4745
  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 +4773,8 @@ class CapabilityRegistry {
4666
4773
 
4667
4774
  // src/agent/capabilities/desktop/cua-driver.ts
4668
4775
  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";
4776
+ import { existsSync as existsSync3, readFileSync as readFileSync6, realpathSync, rmSync as rmSync2, statSync as statSync2 } from "fs";
4777
+ import { homedir as homedir3, platform as platform5, tmpdir } from "os";
4671
4778
  import { basename as basename2, dirname as dirname3, join as join5 } from "path";
4672
4779
  var MAX_DRIVER_OUTPUT_BYTES = 5 * 1024 * 1024;
4673
4780
  var DRIVER_ENV_KEYS = [
@@ -4785,7 +4892,7 @@ function execFile(cmd, args, timeoutMs = 30000, env = driverEnvironment()) {
4785
4892
  clearTimeout(timer);
4786
4893
  if (code !== 0) {
4787
4894
  const detail = stderr.trim() || stdout.trim();
4788
- reject(new Error(`${cmd} failed (${code})${detail ? `: ${detail}` : ""}`));
4895
+ reject(new Error(`${basename2(cmd)} failed (${code})${detail ? `: ${detail}` : ""}`));
4789
4896
  } else {
4790
4897
  resolve2({ stdout, stderr });
4791
4898
  }
@@ -4833,7 +4940,8 @@ class CuaDriver {
4833
4940
  this.binary = options.binary ?? null;
4834
4941
  this.embedded = options.embedded;
4835
4942
  const uid = typeof process.getuid === "function" ? process.getuid() : "user";
4836
- this.socketPath = options.socketPath ?? join5(tmpdir(), `beeuni-cua-${uid}.sock`);
4943
+ const home = serviceLabelFor(agentTunnelHome()).slice(SERVICE_LABEL.length);
4944
+ this.socketPath = options.socketPath ?? join5(tmpdir(), `beeuni-cua-${uid}${home}.sock`);
4837
4945
  this.hostBundleId = options.hostBundleId;
4838
4946
  }
4839
4947
  isEmbedded(binary) {
@@ -4913,7 +5021,9 @@ class CuaDriver {
4913
5021
  const bin = await this.ensureInstalled();
4914
5022
  if (this.isEmbedded(bin)) {
4915
5023
  await this.startEmbeddedDaemon(bin);
4916
- } else if (platform4() === "darwin") {
5024
+ this.daemonReady = true;
5025
+ return { ok: true };
5026
+ } else if (platform5() === "darwin") {
4917
5027
  const child = spawn4("open", ["-n", "-g", "-a", "CuaDriver", "--args", "serve"], {
4918
5028
  detached: true,
4919
5029
  stdio: "ignore",
@@ -4942,6 +5052,12 @@ class CuaDriver {
4942
5052
  if (this.daemon && this.daemon.exitCode === null)
4943
5053
  return;
4944
5054
  const { env, socket } = this.mode(bin);
5055
+ if (existsSync3(this.socketPath)) {
5056
+ await execFile(bin, ["stop", ...socket], 1e4, env).catch(() => {
5057
+ return;
5058
+ });
5059
+ rmSync2(this.socketPath, { force: true });
5060
+ }
4945
5061
  const child = spawn4(bin, ["serve", "--embedded", "--no-permissions-gate", ...socket], {
4946
5062
  stdio: "ignore",
4947
5063
  env
@@ -4965,12 +5081,32 @@ class CuaDriver {
4965
5081
  throw new Error("cua-driver did not start (no socket after 10 s)");
4966
5082
  }
4967
5083
  }
5084
+ async permissionOwner() {
5085
+ return this.isEmbedded(await this.ensureInstalled()) ? "Beeuni" : "CuaDriver";
5086
+ }
5087
+ async missingPermissions() {
5088
+ const report = await this.call("check_permissions", {});
5089
+ const missing = [];
5090
+ if (report?.accessibility === false)
5091
+ missing.push("Accessibility");
5092
+ if (report?.screen_recording === false)
5093
+ missing.push("Screen Recording");
5094
+ return missing;
5095
+ }
4968
5096
  stop() {
4969
5097
  this.daemon?.kill();
4970
5098
  this.daemon = null;
4971
5099
  this.daemonReady = false;
4972
5100
  }
4973
5101
  async ensureDaemonReady() {
5102
+ const bin = await this.ensureInstalled();
5103
+ if (this.isEmbedded(bin)) {
5104
+ if (this.daemon?.exitCode === null && existsSync3(this.socketPath))
5105
+ return;
5106
+ this.stop();
5107
+ await this.startDaemon();
5108
+ return;
5109
+ }
4974
5110
  if (this.daemonReady)
4975
5111
  return;
4976
5112
  try {
@@ -4987,44 +5123,6 @@ class CuaDriver {
4987
5123
  }
4988
5124
 
4989
5125
  // 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
5126
  var LOCAL_ONLY_CUA_TOOLS = new Set(["check_for_update", "install_ffmpeg"]);
5029
5127
  function assertRemotelyCallableTool(tool) {
5030
5128
  if (typeof tool !== "string" || tool.length === 0) {
@@ -5034,24 +5132,30 @@ function assertRemotelyCallableTool(tool) {
5034
5132
  throw new Error(`CUA tool "${tool}" is local-only and cannot run through Agent Tunnel`);
5035
5133
  }
5036
5134
  }
5037
- function createDesktopCapability() {
5135
+
5136
+ class DesktopPermissionMissingError extends Error {
5137
+ code = TunnelErrorCode.DESKTOP_PERMISSION_MISSING;
5138
+ }
5139
+ async function explainFailure(cua, err, onMissing) {
5140
+ const missing = await cua.missingPermissions().catch(() => []);
5141
+ if (missing.length > 0) {
5142
+ onMissing();
5143
+ throw new DesktopPermissionMissingError(`computer_desktop_permission_missing: macOS has not given ${await cua.permissionOwner()} ${missing.join(" and ")} on this computer.`);
5144
+ }
5145
+ throw err;
5146
+ }
5147
+ function createDesktopCapability(options = {}) {
5038
5148
  const cua = new CuaDriver;
5039
5149
  const methods = new Map;
5040
- const assertDesktopPermission = (params, method) => {
5150
+ const onMissing = options.onPermissionMissing ?? (() => {});
5151
+ const assertDesktopPermission = (params) => {
5041
5152
  const permission = params.__permission;
5042
5153
  if (permission?.capability !== "desktop") {
5043
5154
  throw new Error("Permission denied: desktop permission required");
5044
5155
  }
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
5156
  };
5053
5157
  methods.set("desktop.cua.ensure", async (params) => {
5054
- assertDesktopPermission(params, "desktop.cua.ensure");
5158
+ assertDesktopPermission(params);
5055
5159
  const binary = await cua.ensureInstalled();
5056
5160
  const version = await cua.version().catch(() => {
5057
5161
  return;
@@ -5059,40 +5163,36 @@ function createDesktopCapability() {
5059
5163
  return { ok: true, binary, version };
5060
5164
  });
5061
5165
  methods.set("desktop.cua.start_daemon", async (params) => {
5062
- assertDesktopPermission(params, "desktop.cua.start_daemon");
5166
+ assertDesktopPermission(params);
5063
5167
  return cua.startDaemon();
5064
5168
  });
5065
5169
  methods.set("desktop.cua.status", async (params) => {
5066
- assertDesktopPermission(params, "desktop.cua.status");
5170
+ assertDesktopPermission(params);
5067
5171
  return { status: await cua.status() };
5068
5172
  });
5069
5173
  methods.set("desktop.cua.version", async (params) => {
5070
- assertDesktopPermission(params, "desktop.cua.version");
5174
+ assertDesktopPermission(params);
5071
5175
  return { version: await cua.version() };
5072
5176
  });
5073
5177
  methods.set("desktop.cua.list_tools", async (params) => {
5074
- assertDesktopPermission(params, "desktop.cua.list_tools");
5178
+ assertDesktopPermission(params);
5075
5179
  return { tools: await cua.listTools() };
5076
5180
  });
5077
5181
  methods.set("desktop.cua.describe", async (params) => {
5078
- assertDesktopPermission(params, "desktop.cua.describe");
5182
+ assertDesktopPermission(params);
5079
5183
  return { description: await cua.describe(params.tool) };
5080
5184
  });
5081
5185
  methods.set("desktop.cua.call", async (params) => {
5082
- assertDesktopPermission(params, "desktop.cua.call");
5186
+ assertDesktopPermission(params);
5083
5187
  const tool = params.tool;
5084
5188
  assertRemotelyCallableTool(tool);
5085
5189
  const args = params.args || {};
5086
- return cua.call(tool, args);
5190
+ try {
5191
+ return await cua.call(tool, args);
5192
+ } catch (err) {
5193
+ return explainFailure(cua, err, onMissing);
5194
+ }
5087
5195
  });
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
5196
  return {
5097
5197
  name: "desktop",
5098
5198
  methods
@@ -5503,7 +5603,7 @@ function createShellCapability(config) {
5503
5603
  }
5504
5604
 
5505
5605
  // src/agent/capabilities/enabled-registry.ts
5506
- function createEnabledCapabilityRegistry(config, findDesktopDriver = findCuaDriverBinary) {
5606
+ function createEnabledCapabilityRegistry(config, findDesktopDriver = findCuaDriverBinary, desktop = {}) {
5507
5607
  const registry = new CapabilityRegistry;
5508
5608
  const enabled = new Set(config.enabledCapabilities ?? ["filesystem", "shell", "desktop"]);
5509
5609
  if (enabled.has("filesystem"))
@@ -5511,13 +5611,13 @@ function createEnabledCapabilityRegistry(config, findDesktopDriver = findCuaDriv
5511
5611
  if (enabled.has("shell"))
5512
5612
  registry.register(createShellCapability(config));
5513
5613
  if (enabled.has("desktop") && findDesktopDriver()) {
5514
- registry.register(createDesktopCapability());
5614
+ registry.register(createDesktopCapability(desktop));
5515
5615
  }
5516
5616
  return registry;
5517
5617
  }
5518
5618
 
5519
5619
  // 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";
5620
+ 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
5621
  import { join as join8 } from "path";
5522
5622
  var CONFIG_DIR2 = agentTunnelHome();
5523
5623
  var CONFIG_FILE2 = join8(CONFIG_DIR2, "config.json");
@@ -5561,7 +5661,7 @@ function clearSavedCredentials() {
5561
5661
  try {
5562
5662
  existing = JSON.parse(readFileSync7(CONFIG_FILE2, "utf-8"));
5563
5663
  } catch {
5564
- rmSync2(CONFIG_FILE2, { force: true });
5664
+ rmSync3(CONFIG_FILE2, { force: true });
5565
5665
  return true;
5566
5666
  }
5567
5667
  const hadCredentials = PAIRING_FIELDS.some((key) => (key in existing) && existing[key] != null);
@@ -5642,7 +5742,6 @@ function isShellStartupNoise(line) {
5642
5742
  }
5643
5743
 
5644
5744
  // src/agent/prompts.ts
5645
- import { createInterface } from "readline/promises";
5646
5745
  function isInteractiveTerminal() {
5647
5746
  return process.stdin.isTTY === true && process.stdout.isTTY === true;
5648
5747
  }
@@ -5652,41 +5751,16 @@ function isTruthyFlag(value) {
5652
5751
  function anyFlag(flags, names) {
5653
5752
  return names.some((name) => isTruthyFlag(flags[name]));
5654
5753
  }
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
5754
 
5681
5755
  // 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";
5756
+ 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";
5757
+ import { homedir as homedir5, platform as platform7 } from "os";
5684
5758
  import { join as join10 } from "path";
5685
5759
 
5686
5760
  // src/agent/service-drivers.ts
5687
5761
  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";
5762
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync8, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
5763
+ import { homedir as homedir4, platform as platform6, userInfo } from "os";
5690
5764
  import { dirname as dirname6, join as join9 } from "path";
5691
5765
 
5692
5766
  // src/agent/service-quoting.ts
@@ -5835,7 +5909,7 @@ var launchd = {
5835
5909
  const existed = existsSync5(paths.launchdPlist);
5836
5910
  const stop = run("launchctl", ["bootout", launchdTarget(), paths.launchdPlist]);
5837
5911
  if (existed)
5838
- rmSync3(paths.launchdPlist, { force: true });
5912
+ rmSync4(paths.launchdPlist, { force: true });
5839
5913
  return { detail: stop.detail };
5840
5914
  },
5841
5915
  start(paths, installed) {
@@ -5888,7 +5962,7 @@ var systemd = {
5888
5962
  const existed = existsSync5(paths.systemdUnit);
5889
5963
  const disable = run("systemctl", ["--user", "disable", "--now", `${paths.label}.service`]);
5890
5964
  if (existed)
5891
- rmSync3(paths.systemdUnit, { force: true });
5965
+ rmSync4(paths.systemdUnit, { force: true });
5892
5966
  run("systemctl", ["--user", "daemon-reload"]);
5893
5967
  return { detail: disable.detail };
5894
5968
  },
@@ -5963,8 +6037,8 @@ var scheduledTask = {
5963
6037
  const stop = run("schtasks.exe", ["/End", "/TN", paths.label]);
5964
6038
  const del = run("schtasks.exe", ["/Delete", "/TN", paths.label, "/F"]);
5965
6039
  if (existed)
5966
- rmSync3(paths.windowsScript, { force: true });
5967
- rmSync3(windowsTaskXmlPath(paths), { force: true });
6040
+ rmSync4(paths.windowsScript, { force: true });
6041
+ rmSync4(windowsTaskXmlPath(paths), { force: true });
5968
6042
  return { detail: joinDetails(stop, del) };
5969
6043
  },
5970
6044
  start(paths, installed) {
@@ -6010,7 +6084,7 @@ function serviceDriverFor(os) {
6010
6084
  return DRIVERS[os];
6011
6085
  }
6012
6086
  function serviceDriver() {
6013
- return serviceDriverFor(platform5());
6087
+ return serviceDriverFor(platform6());
6014
6088
  }
6015
6089
 
6016
6090
  // src/agent/service.ts
@@ -6046,8 +6120,22 @@ function runnerPartsFor(script, runtime = {
6046
6120
  }
6047
6121
  return { command: runtime.execPath, args: [vendor(script, paths), "run", "--service"], env };
6048
6122
  }
6123
+ function installedAppRunner(apps = ["/Applications/Beeuni.app", join10(homedir5(), "Applications", "Beeuni.app")], os = process.platform) {
6124
+ if (os !== "darwin")
6125
+ return null;
6126
+ for (const app of apps) {
6127
+ const execPath = join10(app, "Contents", "MacOS", "Beeuni");
6128
+ const script = join10(app, "Contents", "Resources", "agent-tunnel", "agent-cli.js");
6129
+ if (existsSync6(execPath) && existsSync6(script))
6130
+ return { execPath, script };
6131
+ }
6132
+ return null;
6133
+ }
6049
6134
  var plannedRunner = (script, paths) => isEphemeralRunnerPath(script) ? paths.vendoredRunner : script;
6050
6135
  function currentRunnerParts(vendor = vendorRunner) {
6136
+ const app = process.versions.electron ? null : installedAppRunner();
6137
+ if (app)
6138
+ return runnerPartsFor(app.script, { execPath: app.execPath, electron: "installed-app" }, getServicePaths(), vendor);
6051
6139
  const script = process.argv[1];
6052
6140
  if (script && existsSync6(script))
6053
6141
  return runnerPartsFor(script, undefined, getServicePaths(), vendor);
@@ -6062,7 +6150,7 @@ function withDriver(operate, fallback) {
6062
6150
  const installed = existsSync6(path);
6063
6151
  const outcome = operate(driver, paths, installed);
6064
6152
  return {
6065
- platform: platform6(),
6153
+ platform: platform7(),
6066
6154
  installed: outcome.installed ?? installed,
6067
6155
  active: outcome.active ?? fallback.active,
6068
6156
  ...outcome.enabled !== undefined ? { enabled: outcome.enabled } : {},
@@ -6080,7 +6168,7 @@ function installService() {
6080
6168
  return withDriver((driver, servicePaths) => driver.install(servicePaths, runner), { installed: false, active: null });
6081
6169
  }
6082
6170
  function uninstallService() {
6083
- rmSync4(getServicePaths().binDir, { recursive: true, force: true });
6171
+ rmSync5(getServicePaths().binDir, { recursive: true, force: true });
6084
6172
  return withDriver((driver, paths) => ({ ...driver.uninstall(paths), installed: false, active: false }), { installed: false, active: false });
6085
6173
  }
6086
6174
  function startService() {
@@ -6108,7 +6196,7 @@ function restartService() {
6108
6196
  function getServiceStatus() {
6109
6197
  if (!serviceDriver()) {
6110
6198
  return {
6111
- platform: platform6(),
6199
+ platform: platform7(),
6112
6200
  installed: false,
6113
6201
  active: null,
6114
6202
  detail: SUPPORTED_PLATFORMS_MESSAGE
@@ -6165,6 +6253,35 @@ function renderServiceAction(action, status) {
6165
6253
  blankLine();
6166
6254
  }
6167
6255
 
6256
+ // src/agent/self-restart.ts
6257
+ import { statSync as statSync3 } from "fs";
6258
+ function runtimeFingerprint(files) {
6259
+ return files.map((file) => {
6260
+ try {
6261
+ const stat2 = statSync3(file);
6262
+ return `${stat2.ino}:${stat2.mtimeMs}:${stat2.size}`;
6263
+ } catch {
6264
+ return "missing";
6265
+ }
6266
+ }).join("|");
6267
+ }
6268
+ var PERMISSION_RESTART_MIN_UPTIME_S = 5 * 60;
6269
+ function watchForRestart(exit, intervalMs = 60000) {
6270
+ const files = [process.execPath, process.argv[1]].filter((file) => Boolean(file));
6271
+ const started = runtimeFingerprint(files);
6272
+ setInterval(() => {
6273
+ if (runtimeFingerprint(files) !== started)
6274
+ exit("its code was updated");
6275
+ }, intervalMs).unref();
6276
+ return {
6277
+ permissionMissing: () => {
6278
+ if (process.uptime() < PERMISSION_RESTART_MIN_UPTIME_S)
6279
+ return;
6280
+ setTimeout(() => exit("macOS refused a desktop permission"), 1000).unref();
6281
+ }
6282
+ };
6283
+ }
6284
+
6168
6285
  // src/agent/state-file.ts
6169
6286
  import { chmodSync as chmodSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync10, renameSync as renameSync3, writeFileSync as writeFileSync6 } from "fs";
6170
6287
  import { join as join11 } from "path";
@@ -6191,10 +6308,22 @@ function writeAgentState(status, tunnel, home = agentTunnelHome()) {
6191
6308
  renameSync3(tmp, stateFilePath(home));
6192
6309
  } catch {}
6193
6310
  }
6311
+ function isAlive2(pid) {
6312
+ if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0)
6313
+ return false;
6314
+ try {
6315
+ process.kill(pid, 0);
6316
+ return true;
6317
+ } catch (error) {
6318
+ return error.code === "EPERM";
6319
+ }
6320
+ }
6194
6321
  function readAgentState(home = agentTunnelHome()) {
6195
6322
  try {
6196
6323
  const parsed = JSON.parse(readFileSync10(stateFilePath(home), "utf8"));
6197
- return parsed && typeof parsed === "object" && typeof parsed.status === "string" ? parsed : null;
6324
+ if (!parsed || typeof parsed !== "object" || typeof parsed.status !== "string")
6325
+ return null;
6326
+ return parsed.status !== "offline" && !isAlive2(parsed.pid) ? { ...parsed, status: "offline" } : parsed;
6198
6327
  } catch {
6199
6328
  return null;
6200
6329
  }
@@ -6207,13 +6336,18 @@ var FOREGROUND_FLAGS = ["foreground", "no-daemon", "no-service", "no-background"
6207
6336
  var DEFAULT_LOG_LINES = 60;
6208
6337
  function parseArgs(argv) {
6209
6338
  const flags = {};
6339
+ const words = [];
6210
6340
  for (let i = 3;i < argv.length; i++) {
6211
6341
  const arg = argv[i];
6212
- if (!arg.startsWith("--"))
6342
+ if (!arg.startsWith("--")) {
6343
+ words.push(arg);
6213
6344
  continue;
6345
+ }
6214
6346
  const next = argv[i + 1];
6215
6347
  flags[arg.slice(2)] = next && !next.startsWith("--") ? argv[++i] : "true";
6216
6348
  }
6349
+ if (words.length > 0)
6350
+ flags._ = words.join(" ");
6217
6351
  return { command: argv[2] || "help", flags };
6218
6352
  }
6219
6353
  var jsonMode = false;
@@ -6238,7 +6372,16 @@ function startAgent(config, options = {}) {
6238
6372
  process.stdout.write = process.stderr.write.bind(process.stderr);
6239
6373
  options = { ...options, service: true };
6240
6374
  }
6241
- const registry = createEnabledCapabilityRegistry(config);
6375
+ let agent = null;
6376
+ const restart = options.service ? watchForRestart((reason) => {
6377
+ process.stdout.write(`[agent-tunnel] restarting: ${reason}
6378
+ `);
6379
+ agent?.disconnect();
6380
+ process.exit(0);
6381
+ }) : null;
6382
+ const registry = createEnabledCapabilityRegistry(config, undefined, {
6383
+ onPermissionMissing: restart?.permissionMissing
6384
+ });
6242
6385
  if (config.enabledCapabilities?.includes("desktop") && !registry.has("desktop")) {
6243
6386
  console.error("[agent-tunnel] Computer Use is approved but unavailable: install the trusted cua-driver locally, then restart Agent Tunnel.");
6244
6387
  }
@@ -6254,7 +6397,7 @@ function startAgent(config, options = {}) {
6254
6397
  version: agentTunnelVersion()
6255
6398
  });
6256
6399
  }
6257
- const agent = new TunnelAgent(config, registry, { onStatus: (status, current) => writeAgentState(status, current) }, {
6400
+ agent = new TunnelAgent(config, registry, { onStatus: (status, current) => writeAgentState(status, current) }, {
6258
6401
  persistent: options.service === true,
6259
6402
  reloadConfig: () => loadConfig({ apiUrl: config.apiUrl })
6260
6403
  });
@@ -6263,31 +6406,37 @@ function startAgent(config, options = {}) {
6263
6406
  if (!options.service)
6264
6407
  console.log(`
6265
6408
  ${c.dim} Shutting down…${c.reset}`);
6266
- agent.disconnect();
6409
+ agent?.disconnect();
6267
6410
  process.exit(0);
6268
6411
  };
6269
6412
  process.on("SIGTERM", shutdown);
6270
6413
  process.on("SIGINT", shutdown);
6271
6414
  }
6272
- async function chooseBackgroundMode(flags) {
6415
+ function chooseBackgroundMode(flags) {
6273
6416
  if (anyFlag(flags, BACKGROUND_FLAGS))
6274
6417
  return true;
6275
6418
  if (anyFlag(flags, FOREGROUND_FLAGS))
6276
6419
  return false;
6277
- if (jsonMode || !isInteractiveTerminal())
6278
- return false;
6420
+ return !jsonMode && isInteractiveTerminal();
6421
+ }
6422
+ function printServiceControls() {
6423
+ const npx = "npx @beeuni/agent-tunnel@latest";
6424
+ console.log(` ${c.dim}Runs in the background: starts at login, restarts after failures, keeps running after this terminal closes.${c.reset}`);
6425
+ if (installedAppRunner()) {
6426
+ 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}`);
6427
+ }
6279
6428
  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}`);
6429
+ field("Pause", `${c.white}${npx} stop${c.reset}`);
6430
+ field("Resume", `${c.white}${npx} start${c.reset}`);
6431
+ field("Disconnect", `${c.white}${npx} logout${c.reset}`);
6283
6432
  blankLine();
6284
- return promptYesNo(" Install the background service now?", DEFAULT_INSTALL_BACKGROUND_SERVICE);
6285
6433
  }
6286
6434
  async function launch(config, flags, lease) {
6287
- if (await chooseBackgroundMode(flags)) {
6435
+ if (chooseBackgroundMode(flags)) {
6288
6436
  saveCredentials(config.tunnelId, config.token, config.apiUrl);
6289
6437
  if (!jsonMode) {
6290
6438
  renderServiceAction("install", SERVICE_ACTIONS.install.run());
6439
+ printServiceControls();
6291
6440
  return;
6292
6441
  }
6293
6442
  try {
@@ -6495,7 +6644,7 @@ function commandStatus(flags) {
6495
6644
  serviceLabel: getServicePaths().label,
6496
6645
  service,
6497
6646
  state: readAgentState(),
6498
- access: readAccess(),
6647
+ access: accessView(),
6499
6648
  lastActivity: lastServiceActivity()
6500
6649
  }, null, 2));
6501
6650
  return;
@@ -6552,8 +6701,8 @@ async function unpairOnServer() {
6552
6701
  async function commandLogout(flags) {
6553
6702
  const serverUnpaired = await unpairOnServer();
6554
6703
  const removed = clearSavedCredentials();
6555
- rmSync5(accessFilePath(), { force: true });
6556
- rmSync5(accessRequestPath(), { force: true });
6704
+ rmSync6(accessFilePath(), { force: true });
6705
+ rmSync6(accessRequestPath(), { force: true });
6557
6706
  const keepService = isTruthyFlag(flags["keep-service"]);
6558
6707
  if (!keepService)
6559
6708
  SERVICE_ACTIONS.uninstall.run();
@@ -6624,12 +6773,74 @@ function commandServiceAction(action, flags) {
6624
6773
  }
6625
6774
  renderServiceAction(action, SERVICE_ACTIONS[action].run());
6626
6775
  }
6776
+ function describeAccess(view) {
6777
+ const clock = (iso) => new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
6778
+ if (view.mode === "always")
6779
+ return `${c.white}Always allowed${c.reset}`;
6780
+ if (view.mode === "off")
6781
+ return `${c.white}Off${c.reset} ${c.dim}(every agent call is refused)${c.reset}`;
6782
+ const now = view.grantedUntil ? `allowed until ${clock(view.grantedUntil)}` : view.deniedUntil ? `denied until ${clock(view.deniedUntil)}` : "asks before the next call";
6783
+ return `${c.white}Ask each time${c.reset} ${c.dim}· ${now}${c.reset}`;
6784
+ }
6785
+ function printAccess(view, flags) {
6786
+ if (isTruthyFlag(flags.json)) {
6787
+ console.log(JSON.stringify(view, null, 2));
6788
+ return;
6789
+ }
6790
+ const npx = "npx @beeuni/agent-tunnel@latest";
6791
+ blankLine();
6792
+ field("access", describeAccess(view));
6793
+ 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}`);
6794
+ if (view.pendingRequest) {
6795
+ field("waiting", `${c.white}${view.pendingRequest.change ? "a change asked for through Beeuni" : `an agent call (${view.pendingRequest.capability})`}${c.reset}`);
6796
+ field("", `${c.dim}${npx} access --allow-for 60 or --deny${c.reset}`);
6797
+ }
6798
+ blankLine();
6799
+ 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}`);
6800
+ blankLine();
6801
+ }
6802
+ function commandAccess(flags) {
6803
+ const change = {};
6804
+ if (flags._)
6805
+ change.mode = flags._;
6806
+ if (flags["allow-for"] !== undefined) {
6807
+ change.grantMinutes = Number(flags["allow-for"]);
6808
+ if (!(change.grantMinutes > 0))
6809
+ fail("--allow-for takes minutes, at most 1440. Example: agent-tunnel access --allow-for 60");
6810
+ }
6811
+ if (isTruthyFlag(flags.revoke))
6812
+ change.revoke = true;
6813
+ if (isTruthyFlag(flags.deny))
6814
+ change.deny = true;
6815
+ if (Object.keys(change).length === 0)
6816
+ return printAccess(accessView(), flags);
6817
+ if (change.mode !== undefined && !["ask", "always", "off"].includes(change.mode)) {
6818
+ fail("Access is one of: ask, always, off. Example: agent-tunnel access ask");
6819
+ }
6820
+ if (change.grantMinutes !== undefined && change.mode === undefined && readAccess().mode !== "ask")
6821
+ change.mode = "ask";
6822
+ printAccess(applyAccess(change), flags);
6823
+ }
6824
+ function commandKeepAwake(flags) {
6825
+ if (flags._ === undefined)
6826
+ return printAccess(accessView(), flags);
6827
+ if (flags._ !== "on" && flags._ !== "off")
6828
+ fail("Keep awake is on or off. Example: agent-tunnel keep-awake on");
6829
+ if (!accessView().keepAwakeSupported)
6830
+ fail("Keep awake is not available on this system yet.");
6831
+ printAccess(applyAccess({ keepAwake: flags._ === "on" }), flags);
6832
+ }
6627
6833
  var COMMANDS = {
6628
6834
  connect: {
6629
6835
  summary: "Pair this machine, then run it in the background or this terminal",
6630
6836
  run: commandConnect
6631
6837
  },
6632
6838
  status: { summary: "Show pairing, capabilities, and service state (--json)", run: commandStatus },
6839
+ access: {
6840
+ summary: "When Beeuni may use this computer: ask, always, off (--allow-for <min>, --revoke, --deny, --json)",
6841
+ run: commandAccess
6842
+ },
6843
+ "keep-awake": { summary: "Keep this computer awake while connected: on, off (--json)", run: commandKeepAwake },
6633
6844
  logs: { summary: "Show recent service logs (--lines <n>, --all, --clear)", run: commandLogs },
6634
6845
  start: {
6635
6846
  summary: "Start the background service (resumes a paused one)",
@@ -6668,7 +6879,7 @@ var OPTIONS = [
6668
6879
  ["--reauth", "With connect: discard the saved credential and pair again"],
6669
6880
  ["--daemon / --foreground", "With connect: skip the prompt and choose the mode"],
6670
6881
  ["--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"],
6882
+ ["--json", "With connect: NDJSON events, no prompts. With status, access, keep-awake, and logout: machine-readable output"],
6672
6883
  ["--keep-service", "With logout: keep the background service installed"]
6673
6884
  ];
6674
6885
  function showHelp() {
@@ -6700,14 +6911,14 @@ function showHelp() {
6700
6911
  var { command, flags } = parseArgs(process.argv);
6701
6912
  jsonMode = command === "connect" && isTruthyFlag(flags.json);
6702
6913
  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.`);
6914
+ console.error(` ${glyph.bad} --keep-awake is not a flag. Run: agent-tunnel keep-awake on`);
6704
6915
  process.exit(2);
6705
6916
  }
6706
6917
  var resolved = COMMANDS[command] ?? Object.values(COMMANDS).find((entry) => entry.aliases?.includes(command));
6707
6918
  if (!resolved) {
6708
6919
  showHelp();
6709
6920
  } else {
6710
- Promise.resolve(resolved.run(flags)).catch((error) => {
6921
+ Promise.resolve().then(() => resolved.run(flags)).catch((error) => {
6711
6922
  fail(error instanceof Error ? error.message : String(error));
6712
6923
  });
6713
6924
  }