bdy 1.23.6-dev → 1.23.7-dev

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 (205) hide show
  1. package/distTs/package.json +10 -2
  2. package/distTs/src/agent/linux.js +53 -3
  3. package/distTs/src/agent/manager.js +3 -6
  4. package/distTs/src/agent/osx.js +51 -3
  5. package/distTs/src/agent/system.js +400 -21
  6. package/distTs/src/agent/windows.js +4 -7
  7. package/distTs/src/command/agent/debug.js +2 -2
  8. package/distTs/src/command/agent/install.js +37 -10
  9. package/distTs/src/command/agent/restart.js +2 -2
  10. package/distTs/src/command/agent/run.js +3 -0
  11. package/distTs/src/command/agent/start.js +2 -2
  12. package/distTs/src/command/agent/status.js +19 -0
  13. package/distTs/src/command/agent/stop.js +2 -2
  14. package/distTs/src/command/agent/uninstall.js +5 -8
  15. package/distTs/src/command/agent/update.js +2 -2
  16. package/distTs/src/command/artifact/publish.js +3 -1
  17. package/distTs/src/command/version.js +13 -5
  18. package/distTs/src/logger.js +40 -4
  19. package/distTs/src/texts.js +35 -19
  20. package/distTs/src/utils.js +98 -3
  21. package/node_modules/asn1/Jenkinsfile +65 -0
  22. package/node_modules/asn1/LICENSE +19 -0
  23. package/node_modules/asn1/README.md +50 -0
  24. package/node_modules/asn1/lib/ber/errors.js +13 -0
  25. package/node_modules/asn1/lib/ber/index.js +27 -0
  26. package/node_modules/asn1/lib/ber/reader.js +262 -0
  27. package/node_modules/asn1/lib/ber/types.js +36 -0
  28. package/node_modules/asn1/lib/ber/writer.js +317 -0
  29. package/node_modules/asn1/lib/index.js +20 -0
  30. package/node_modules/asn1/package.json +31 -0
  31. package/node_modules/bcrypt-pbkdf/CONTRIBUTING.md +13 -0
  32. package/node_modules/bcrypt-pbkdf/LICENSE +66 -0
  33. package/node_modules/bcrypt-pbkdf/README.md +45 -0
  34. package/node_modules/bcrypt-pbkdf/index.js +556 -0
  35. package/node_modules/bcrypt-pbkdf/package.json +15 -0
  36. package/node_modules/safer-buffer/LICENSE +21 -0
  37. package/node_modules/safer-buffer/Porting-Buffer.md +268 -0
  38. package/node_modules/safer-buffer/Readme.md +156 -0
  39. package/node_modules/safer-buffer/dangerous.js +58 -0
  40. package/node_modules/safer-buffer/package.json +34 -0
  41. package/node_modules/safer-buffer/safer.js +77 -0
  42. package/node_modules/safer-buffer/tests.js +406 -0
  43. package/node_modules/ssh2/.eslintignore +4 -0
  44. package/node_modules/ssh2/.eslintrc.js +5 -0
  45. package/node_modules/ssh2/.github/workflows/ci.yml +110 -0
  46. package/node_modules/ssh2/.github/workflows/lint.yml +27 -0
  47. package/node_modules/ssh2/LICENSE +19 -0
  48. package/node_modules/ssh2/README.md +1529 -0
  49. package/node_modules/ssh2/SFTP.md +413 -0
  50. package/node_modules/ssh2/examples/server-chat.js +238 -0
  51. package/node_modules/ssh2/examples/sftp-server-download-only.js +134 -0
  52. package/node_modules/ssh2/install.js +27 -0
  53. package/node_modules/ssh2/lib/Channel.js +295 -0
  54. package/node_modules/ssh2/lib/agent.js +1123 -0
  55. package/node_modules/ssh2/lib/client.js +2176 -0
  56. package/node_modules/ssh2/lib/http-agents.js +84 -0
  57. package/node_modules/ssh2/lib/index.js +44 -0
  58. package/node_modules/ssh2/lib/keygen.js +582 -0
  59. package/node_modules/ssh2/lib/protocol/Protocol.js +2136 -0
  60. package/node_modules/ssh2/lib/protocol/SFTP.js +4052 -0
  61. package/node_modules/ssh2/lib/protocol/constants.js +356 -0
  62. package/node_modules/ssh2/lib/protocol/crypto/binding.gyp +23 -0
  63. package/node_modules/ssh2/lib/protocol/crypto/poly1305.js +43 -0
  64. package/node_modules/ssh2/lib/protocol/crypto/src/binding.cc +2300 -0
  65. package/node_modules/ssh2/lib/protocol/crypto.js +1602 -0
  66. package/node_modules/ssh2/lib/protocol/handlers.js +16 -0
  67. package/node_modules/ssh2/lib/protocol/handlers.misc.js +1285 -0
  68. package/node_modules/ssh2/lib/protocol/kex.js +1908 -0
  69. package/node_modules/ssh2/lib/protocol/keyParser.js +1484 -0
  70. package/node_modules/ssh2/lib/protocol/node-fs-compat.js +115 -0
  71. package/node_modules/ssh2/lib/protocol/utils.js +356 -0
  72. package/node_modules/ssh2/lib/protocol/zlib.js +255 -0
  73. package/node_modules/ssh2/lib/server.js +1380 -0
  74. package/node_modules/ssh2/lib/utils.js +336 -0
  75. package/node_modules/ssh2/package.json +49 -0
  76. package/node_modules/ssh2/test/common.js +316 -0
  77. package/node_modules/ssh2/test/fixtures/bad_rsa_private_key +26 -0
  78. package/node_modules/ssh2/test/fixtures/https_cert.pem +33 -0
  79. package/node_modules/ssh2/test/fixtures/https_key.pem +52 -0
  80. package/node_modules/ssh2/test/fixtures/id_dsa +12 -0
  81. package/node_modules/ssh2/test/fixtures/id_ecdsa +5 -0
  82. package/node_modules/ssh2/test/fixtures/id_rsa +15 -0
  83. package/node_modules/ssh2/test/fixtures/id_rsa.ppk +26 -0
  84. package/node_modules/ssh2/test/fixtures/id_rsa_enc +30 -0
  85. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_dsa +21 -0
  86. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_dsa.pub +1 -0
  87. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_dsa.pub.result +7 -0
  88. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_dsa.result +7 -0
  89. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_dsa_enc +22 -0
  90. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_dsa_enc.pub +1 -0
  91. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_dsa_enc.pub.result +7 -0
  92. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_dsa_enc.result +7 -0
  93. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_dsa_enc_gcm +23 -0
  94. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_dsa_enc_gcm.pub +1 -0
  95. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_dsa_enc_gcm.pub.result +7 -0
  96. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_dsa_enc_gcm.result +7 -0
  97. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ecdsa +9 -0
  98. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ecdsa.pub +1 -0
  99. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ecdsa.pub.result +7 -0
  100. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ecdsa.result +7 -0
  101. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ecdsa_enc +10 -0
  102. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ecdsa_enc.pub +1 -0
  103. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ecdsa_enc.pub.result +8 -0
  104. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ecdsa_enc.result +7 -0
  105. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ecdsa_enc_gcm +10 -0
  106. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ecdsa_enc_gcm.pub +1 -0
  107. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ecdsa_enc_gcm.pub.result +8 -0
  108. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ecdsa_enc_gcm.result +7 -0
  109. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ed25519 +7 -0
  110. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ed25519.pub +1 -0
  111. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ed25519.pub.result +7 -0
  112. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ed25519.result +7 -0
  113. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_rsa +27 -0
  114. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_rsa.pub +1 -0
  115. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_rsa.pub.result +7 -0
  116. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_rsa.result +7 -0
  117. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_rsa_enc +28 -0
  118. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_rsa_enc.pub +1 -0
  119. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_rsa_enc.pub.result +8 -0
  120. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_rsa_enc.result +7 -0
  121. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_rsa_enc_gcm +29 -0
  122. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_rsa_enc_gcm.pub +1 -0
  123. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_rsa_enc_gcm.pub.result +8 -0
  124. package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_rsa_enc_gcm.result +7 -0
  125. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_dsa +12 -0
  126. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_dsa.pub +1 -0
  127. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_dsa.pub.result +7 -0
  128. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_dsa.result +7 -0
  129. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_dsa_enc +15 -0
  130. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_dsa_enc.pub +1 -0
  131. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_dsa_enc.pub.result +8 -0
  132. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_dsa_enc.result +7 -0
  133. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_ecdsa +5 -0
  134. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_ecdsa.pub +1 -0
  135. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_ecdsa.pub.result +7 -0
  136. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_ecdsa.result +7 -0
  137. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_ecdsa_enc +8 -0
  138. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_ecdsa_enc.pub +1 -0
  139. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_ecdsa_enc.pub.result +8 -0
  140. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_ecdsa_enc.result +7 -0
  141. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_rsa +27 -0
  142. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_rsa.pub +1 -0
  143. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_rsa.pub.result +7 -0
  144. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_rsa.result +7 -0
  145. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_rsa_enc +30 -0
  146. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_rsa_enc.pub +1 -0
  147. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_rsa_enc.pub.result +8 -0
  148. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_rsa_enc.result +7 -0
  149. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_rsa_enc_aes256 +54 -0
  150. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_rsa_enc_aes256.pub +1 -0
  151. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_rsa_enc_aes256.pub.result +6 -0
  152. package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_rsa_enc_aes256.result +6 -0
  153. package/node_modules/ssh2/test/fixtures/keyParser/ppk_dsa_enc +17 -0
  154. package/node_modules/ssh2/test/fixtures/keyParser/ppk_dsa_enc.result +7 -0
  155. package/node_modules/ssh2/test/fixtures/keyParser/ppk_rsa +26 -0
  156. package/node_modules/ssh2/test/fixtures/keyParser/ppk_rsa.result +7 -0
  157. package/node_modules/ssh2/test/fixtures/keyParser/ppk_rsa_enc +18 -0
  158. package/node_modules/ssh2/test/fixtures/keyParser/ppk_rsa_enc.result +7 -0
  159. package/node_modules/ssh2/test/fixtures/keyParser/rfc4716_rsa.pub +9 -0
  160. package/node_modules/ssh2/test/fixtures/keyParser/rfc4716_rsa.pub.result +7 -0
  161. package/node_modules/ssh2/test/fixtures/keyParser/rfc4716_rsa2.pub +10 -0
  162. package/node_modules/ssh2/test/fixtures/keyParser/rfc4716_rsa2.pub.result +7 -0
  163. package/node_modules/ssh2/test/fixtures/keyParser/rfc4716_rsa3.pub +11 -0
  164. package/node_modules/ssh2/test/fixtures/keyParser/rfc4716_rsa3.pub.result +7 -0
  165. package/node_modules/ssh2/test/fixtures/keyParser/rfc4716_rsa4.pub +11 -0
  166. package/node_modules/ssh2/test/fixtures/keyParser/rfc4716_rsa4.pub.result +7 -0
  167. package/node_modules/ssh2/test/fixtures/keyParser/rfc4716_rsa5.pub +8 -0
  168. package/node_modules/ssh2/test/fixtures/keyParser/rfc4716_rsa5.pub.result +7 -0
  169. package/node_modules/ssh2/test/fixtures/keyParser/rfc4716_rsa6.pub +13 -0
  170. package/node_modules/ssh2/test/fixtures/keyParser/rfc4716_rsa6.pub.result +7 -0
  171. package/node_modules/ssh2/test/fixtures/openssh_new_rsa +27 -0
  172. package/node_modules/ssh2/test/fixtures/ssh_host_dsa_key +12 -0
  173. package/node_modules/ssh2/test/fixtures/ssh_host_ecdsa_key +5 -0
  174. package/node_modules/ssh2/test/fixtures/ssh_host_rsa_key +15 -0
  175. package/node_modules/ssh2/test/test-exec.js +578 -0
  176. package/node_modules/ssh2/test/test-integration-openssh.js +486 -0
  177. package/node_modules/ssh2/test/test-keygen.js +60 -0
  178. package/node_modules/ssh2/test/test-misc-client-server.js +1460 -0
  179. package/node_modules/ssh2/test/test-openssh.js +261 -0
  180. package/node_modules/ssh2/test/test-protocol-crypto.js +631 -0
  181. package/node_modules/ssh2/test/test-protocol-keyparser.js +177 -0
  182. package/node_modules/ssh2/test/test-server-hostkeys.js +138 -0
  183. package/node_modules/ssh2/test/test-sftp.js +842 -0
  184. package/node_modules/ssh2/test/test-shell.js +109 -0
  185. package/node_modules/ssh2/test/test-userauth-agent-openssh.js +110 -0
  186. package/node_modules/ssh2/test/test-userauth-agent.js +171 -0
  187. package/node_modules/ssh2/test/test-userauth.js +611 -0
  188. package/node_modules/ssh2/test/test-worker-imports.js +25 -0
  189. package/node_modules/ssh2/test/test.js +20 -0
  190. package/node_modules/ssh2/util/build_pagent.bat +2 -0
  191. package/node_modules/ssh2/util/pagent.c +88 -0
  192. package/node_modules/ssh2/util/pagent.exe +0 -0
  193. package/node_modules/tweetnacl/.npmignore +4 -0
  194. package/node_modules/tweetnacl/AUTHORS.md +28 -0
  195. package/node_modules/tweetnacl/CHANGELOG.md +221 -0
  196. package/node_modules/tweetnacl/LICENSE +24 -0
  197. package/node_modules/tweetnacl/PULL_REQUEST_TEMPLATE.md +20 -0
  198. package/node_modules/tweetnacl/README.md +459 -0
  199. package/node_modules/tweetnacl/nacl-fast.js +2388 -0
  200. package/node_modules/tweetnacl/nacl-fast.min.js +2 -0
  201. package/node_modules/tweetnacl/nacl.d.ts +98 -0
  202. package/node_modules/tweetnacl/nacl.js +1175 -0
  203. package/node_modules/tweetnacl/nacl.min.js +1 -0
  204. package/node_modules/tweetnacl/package.json +58 -0
  205. package/package.json +10 -2
@@ -12,6 +12,10 @@ const logger_1 = __importDefault(require("../logger"));
12
12
  const manager_1 = __importDefault(require("./manager"));
13
13
  const output_1 = __importDefault(require("../output"));
14
14
  class AgentSystem {
15
+ // Set by `agent run`, which is only ever started by the service - see getServiceConfig in
16
+ // the platform classes. Nothing else needs it: a standalone agent runs in the process that
17
+ // installed or started it, and that one resolves its own directory from disk.
18
+ serviceMode = false;
15
19
  // extend
16
20
  /* eslint-disable @typescript-eslint/no-unused-vars */
17
21
  async install(_id, _host, _token, _port, _start, _user, _pass, _debug) {
@@ -36,14 +40,125 @@ class AgentSystem {
36
40
  getSystemConfigPath() {
37
41
  return '';
38
42
  }
43
+ // Where a standalone install kept its lock before those files moved to the user
44
+ // directory. Still the path for an install made back then - see isUserOwnedInstall.
39
45
  getSystemStandalonePath() {
40
- return '';
46
+ return path_1.default.join(this.getSystemConfigDir(), 'pid.lock');
41
47
  }
42
48
  // extend
43
49
  getSystemConfigDir() {
44
50
  return '';
45
51
  }
52
+ // `--app` installs no service and runs the agent as the invoking user, so its files
53
+ // belong to that user: a system location was the only thing making the mode need root.
54
+ // They get their own directory rather than sitting next to cfg.json, so nothing here can
55
+ // collide with the legacy `agent.json` that loadNewAgentConfig migrates out of the home
56
+ // directory.
57
+ getStandaloneConfigDir() {
58
+ return path_1.default.join((0, utils_1.getHomeDirectory)(), 'agent');
59
+ }
60
+ getStandaloneConfigPath() {
61
+ return path_1.default.join(this.getStandaloneConfigDir(), 'agent.json');
62
+ }
63
+ getStandalonePidPath() {
64
+ return path_1.default.join(this.getStandaloneConfigDir(), 'pid.lock');
65
+ }
66
+ // True when the install this cli would act on is the user's own, which is the case that
67
+ // needs no admin rights. A service install never is, and neither is a standalone install
68
+ // written before the move: that one is still in the system directory and has to keep
69
+ // being managed from there, root and all.
70
+ isUserOwnedInstall() {
71
+ // A service reads and writes the system directory whatever `--user` it was given, and
72
+ // that user may well have an `--app` install of their own in their home - which is not
73
+ // the install this process is
74
+ if (this.serviceMode)
75
+ return false;
76
+ // One agent per machine, `--app` or service. So when a system config exists it is the
77
+ // install: it is machine-wide and every account can read it, while a config sitting in
78
+ // someone's home next to it is a leftover. Letting that leftover win is what made
79
+ // `sudo bdy agent update` update nothing and `agent user` answer 'not a service'.
80
+ if ((0, node_fs_1.existsSync)(this.getSystemConfigPath()))
81
+ return false;
82
+ try {
83
+ return (0, node_fs_1.existsSync)(this.getStandaloneConfigPath());
84
+ }
85
+ catch {
86
+ // A home directory that cannot even be resolved holds no install
87
+ return false;
88
+ }
89
+ }
90
+ // Every home an `--app` install could be sitting in. The containers are read rather than
91
+ // resolved out of /etc/passwd or dscl: a home somewhere else is a home this cannot see,
92
+ // which is the same class of miss as one it cannot read.
93
+ getHomeDirs() {
94
+ const containers = (0, utils_1.isWindows)()
95
+ ? ['C:\\Users']
96
+ : process.platform === 'darwin'
97
+ ? ['/Users']
98
+ : ['/home'];
99
+ // Root's own, which is not inside any of those
100
+ const homes = (0, utils_1.isWindows)() ? [] : ['/root', '/var/root'];
101
+ containers.forEach((root) => {
102
+ try {
103
+ (0, node_fs_1.readdirSync)(root).forEach((entry) => homes.push(path_1.default.join(root, entry)));
104
+ }
105
+ catch {
106
+ // a container that is not there, or not this account's to read
107
+ }
108
+ });
109
+ return homes;
110
+ }
111
+ getStandaloneConfigPathIn(home) {
112
+ return path_1.default.join(home, utils_1.HOME_DIR_NAME, 'agent', 'agent.json');
113
+ }
114
+ // A standalone install is only visible from the account that made it, so a service install
115
+ // has to go looking rather than land on top of one. Only ever called with root behind it,
116
+ // and best-effort by nature - a home this cannot read holds an install this cannot see, which
117
+ // is why the port check stays as the backstop for whatever is actually running.
118
+ findAppInstalls() {
119
+ if ((0, utils_1.isDocker)())
120
+ return [];
121
+ return this.getHomeDirs().filter((home) => {
122
+ try {
123
+ return (0, node_fs_1.existsSync)(this.getStandaloneConfigPathIn(home));
124
+ }
125
+ catch {
126
+ return false;
127
+ }
128
+ }).map((home) => this.getStandaloneConfigPathIn(home));
129
+ }
130
+ getActiveConfigPath() {
131
+ if (this.isUserOwnedInstall())
132
+ return this.getStandaloneConfigPath();
133
+ return this.getSystemConfigPath();
134
+ }
135
+ getActivePidPath() {
136
+ if (this.isUserOwnedInstall())
137
+ return this.getStandalonePidPath();
138
+ return this.getSystemStandalonePath();
139
+ }
140
+ // Where a config with this `standalone` flag belongs. A standalone config already in the
141
+ // system directory was written before the move and is rewritten in place: a second one in
142
+ // the user directory would leave two configs for one agent, and the one that lost would
143
+ // be the one uninstall still has to find.
144
+ getConfigPathForWrite(standalone) {
145
+ if (!standalone)
146
+ return this.getSystemConfigPath();
147
+ if (!(0, node_fs_1.existsSync)(this.getStandaloneConfigPath()) &&
148
+ (0, node_fs_1.existsSync)(this.getSystemConfigPath())) {
149
+ return this.getSystemConfigPath();
150
+ }
151
+ return this.getStandaloneConfigPath();
152
+ }
153
+ // The lock is read back by the cli command that stops the agent, which for an install in the
154
+ // system directory can be a different account than the one that wrote it - see
155
+ // applySystemModes for the rest of the modes in there.
156
+ getPidFileMode() {
157
+ return this.isUserOwnedInstall() ? 0o600 : 0o644;
158
+ }
46
159
  getNewAgentConfigDir() {
160
+ if (this.isUserOwnedInstall())
161
+ return this.getStandaloneConfigDir();
47
162
  return this.getSystemConfigDir();
48
163
  }
49
164
  getAgentConfigDir() {
@@ -61,9 +176,30 @@ class AgentSystem {
61
176
  return Promise.resolve();
62
177
  }
63
178
  // extend
179
+ // The account the installed service runs as, read back from the service config - the only
180
+ // place it is recorded. An update needs it: it is the one path that re-applies the rights
181
+ // without being told a user, and handing the directory to root there would stop a service
182
+ // that was moved to someone else from writing in it.
183
+ //
184
+ // `''` is root, and `null` means this could not be established - a service config that is
185
+ // missing, unreadable or not in the shape this cli writes. The two must stay apart: treating
186
+ // 'do not know' as 'root' is what would take the directory off a service user we failed to
187
+ // recognise, and its agent would then crash-loop unable to write its own log.
188
+ readServiceUser() {
189
+ return null;
190
+ }
191
+ // extend
64
192
  hasAdminRights() {
65
193
  return Promise.resolve(false);
66
194
  }
195
+ // What the agent commands ask instead of hasAdminRights: managing an install that lives
196
+ // in the user's own directory takes no rights at all - there is no service to control and
197
+ // no system file to rewrite. Everything else still needs them.
198
+ async hasRightsToManage() {
199
+ if (this.isUserOwnedInstall())
200
+ return true;
201
+ return this.hasAdminRights();
202
+ }
67
203
  // extend
68
204
  isSupported() {
69
205
  return Promise.resolve(false);
@@ -101,7 +237,7 @@ class AgentSystem {
101
237
  }
102
238
  fixBinaryRights() {
103
239
  return new Promise((resolve, reject) => {
104
- (0, node_fs_1.chmod)(this.getSystemBinaryPath(), 0o777, (err) => {
240
+ (0, node_fs_1.chmod)(this.getSystemBinaryPath(), 0o755, (err) => {
105
241
  if (err)
106
242
  reject(err);
107
243
  else
@@ -110,11 +246,11 @@ class AgentSystem {
110
246
  });
111
247
  }
112
248
  loadSystemConfig() {
113
- const str = (0, node_fs_1.readFileSync)(this.getSystemConfigPath(), 'utf-8');
249
+ const str = (0, node_fs_1.readFileSync)(this.getActiveConfigPath(), 'utf-8');
114
250
  return JSON.parse(str);
115
251
  }
116
252
  loadStandaloneProcConfig() {
117
- return (0, node_fs_1.readFileSync)(this.getSystemStandalonePath(), 'utf-8');
253
+ return (0, node_fs_1.readFileSync)(this.getActivePidPath(), 'utf-8');
118
254
  }
119
255
  loadNewAgentConfig() {
120
256
  const exists = (0, node_fs_1.existsSync)(this.getAgentConfigPath());
@@ -122,7 +258,7 @@ class AgentSystem {
122
258
  (0, node_fs_1.cpSync)(this.getAgentConfigPath(), this.getNewAgentConfigPath(), {
123
259
  force: true,
124
260
  });
125
- (0, node_fs_1.chmodSync)(this.getNewAgentConfigPath(), 0o666);
261
+ (0, node_fs_1.chmodSync)(this.getNewAgentConfigPath(), 0o600);
126
262
  this.clearAgentFiles();
127
263
  }
128
264
  const str = (0, node_fs_1.readFileSync)(this.getNewAgentConfigPath(), 'utf-8');
@@ -136,21 +272,253 @@ class AgentSystem {
136
272
  (0, node_fs_1.rmSync)(this.getSystemBinaryBackupPath(), { force: true });
137
273
  (0, node_fs_1.rmSync)(this.getUpdateTempDir(), { recursive: true, force: true });
138
274
  }
139
- clearSystemFiles() {
140
- (0, node_fs_1.rmSync)(this.getSystemBinaryPath(), { force: true });
141
- (0, node_fs_1.rmSync)(this.getSystemConfigPath(), { force: true });
275
+ // `force` only silences ENOENT, so one path this process may not delete would otherwise take
276
+ // the rest of the list with it - and these lists span two directories with different owners.
277
+ tryRemove(p, recursive = false) {
278
+ try {
279
+ (0, node_fs_1.rmSync)(p, { force: true, recursive });
280
+ }
281
+ catch (err) {
282
+ // An uninstall reports what it managed to do; a file it cannot delete is not a reason to
283
+ // leave the ones it can
284
+ logger_1.default.error(err);
285
+ }
286
+ }
287
+ // The two locations can hold two different agents - a service one in the system directory, an
288
+ // `--app` one in the home directory of whoever is running this - and root, or an elevated
289
+ // shell on windows, is able to delete either. So each of these clears only its own, host key
290
+ // included: nothing may go looking for that through getNewAgentConfigDir afterwards, because
291
+ // the config that resolution keys on is exactly what these have just removed.
292
+ clearStandaloneFiles() {
293
+ const dir = this.getStandaloneConfigDir();
294
+ this.tryRemove(this.getStandaloneConfigPath());
295
+ this.tryRemove(this.getStandalonePidPath());
296
+ this.tryRemove(path_1.default.join(dir, 'srv.json'));
297
+ }
298
+ // extend
299
+ clearServiceFiles() {
300
+ const dir = this.getSystemConfigDir();
301
+ this.tryRemove(this.getSystemBinaryPath());
302
+ this.tryRemove(this.getSystemConfigPath());
303
+ // The lock of a standalone install made before those files moved out of here
304
+ this.tryRemove(this.getSystemStandalonePath());
305
+ this.tryRemove(path_1.default.join(dir, 'srv.json'));
306
+ // Only a service install ever swaps its own binary - see isStandaloneInstall - so whatever
307
+ // is left of an update in here is always that install's
142
308
  this.clearUpdateFiles();
143
309
  }
310
+ // The install this cli resolved to, and only that one. Callers that already know which kind
311
+ // they are unmaking - an install rolling its own service back - say so directly instead.
312
+ clearSystemFiles() {
313
+ if (this.isUserOwnedInstall())
314
+ this.clearStandaloneFiles();
315
+ else
316
+ this.clearServiceFiles();
317
+ }
144
318
  clearAgentFiles() {
145
319
  (0, node_fs_1.rmSync)(this.getAgentConfigPath(), { force: true });
146
320
  }
147
321
  clearNewAgentFiles() {
148
322
  (0, node_fs_1.rmSync)(this.getNewAgentConfigPath(), { force: true });
149
323
  }
324
+ // Only ever sets the mode on a directory it creates. An install made before the modes were
325
+ // tightened has this at 0777 and its service may be running as a user who can only write in
326
+ // here *because* of that - taking it to 0755 from a command like `agent debug`, which knows
327
+ // nothing about that user, would break the running agent. Tightening an existing directory
328
+ // is applySystemModes' job, and that only runs where the owner is set in the same breath.
150
329
  ensureSystemConfigDir() {
151
330
  const p = this.getSystemConfigDir();
331
+ if ((0, node_fs_1.existsSync)(p))
332
+ return;
152
333
  (0, node_fs_1.mkdirSync)(p, { recursive: true });
153
- (0, node_fs_1.chmodSync)(p, 0o777);
334
+ try {
335
+ (0, node_fs_1.chmodSync)(p, 0o755);
336
+ }
337
+ catch {
338
+ // do nothing, the dir may be owned by the service user rather than by this process
339
+ }
340
+ }
341
+ // fs.chown takes a numeric uid and node has no getpwnam, so the name has to be looked up.
342
+ // Not a shell call: `user` comes off the command line.
343
+ resolveUserIds(user) {
344
+ const { execFile } = require('node:child_process');
345
+ const read = (flag) => new Promise((resolve, reject) => {
346
+ execFile('id', [flag, user], (err, stdout) => {
347
+ const id = Number.parseInt(String(stdout).trim(), 10);
348
+ if (err || Number.isNaN(id)) {
349
+ // `id` failing is how the answer arrives, not something to show: what the caller
350
+ // needs to read is which user could not be found, not the command that looked
351
+ if (err)
352
+ logger_1.default.error(err);
353
+ reject(new Error((0, texts_1.ERR_AGENT_USER_UNKNOWN)(user)));
354
+ }
355
+ else
356
+ resolve(id);
357
+ });
358
+ });
359
+ return Promise.all([read('-u'), read('-g')]).then(([uid, gid]) => ({
360
+ uid,
361
+ gid,
362
+ }));
363
+ }
364
+ // Opens a path without ever following a symlink at the end of it, so the descriptor is bound
365
+ // to the inode this call meant: whatever the name is pointed at afterwards, an fchmod or
366
+ // fchown through this still lands on the file that was checked. O_NONBLOCK because a fifo
367
+ // left here would otherwise hold the open until someone writes to it, and an install that
368
+ // hangs is a worse answer than one that skips a file. Windows has neither flag, and no
369
+ // caller reaches it there.
370
+ openNoFollow(p, directory = false) {
371
+ let flags = node_fs_1.constants.O_RDONLY;
372
+ if (!(0, utils_1.isWindows)())
373
+ flags |= node_fs_1.constants.O_NOFOLLOW | node_fs_1.constants.O_NONBLOCK;
374
+ if (directory && node_fs_1.constants.O_DIRECTORY)
375
+ flags |= node_fs_1.constants.O_DIRECTORY;
376
+ return (0, node_fs_1.openSync)(p, flags);
377
+ }
378
+ // Hands the system directory to the account the service runs as, which is the other half of
379
+ // dropping 0777: everything in here is created by the root process that installs the agent,
380
+ // and the service still has to be able to write its host key, its log and a self-update.
381
+ // Re-run by changeUser, so moving the service to another user keeps working - and the
382
+ // default target is root, so moving it back takes the rights back off the old one.
383
+ //
384
+ // Always runs *before* applySystemModes, never after. Only one of the two can fail first, and
385
+ // the orders are not equally bad: ownership handed over and the mode left at the old 0777
386
+ // leaves everyone able to work, while a tightened mode over the wrong owner locks the service
387
+ // user out of a directory that was serving them a moment earlier.
388
+ async chownSystemDir(user) {
389
+ // Windows has no ownership to hand over this way: the service user is granted access to
390
+ // the same directory with icacls instead, see AgentWindows.grantAccess
391
+ if ((0, utils_1.isWindows)())
392
+ return;
393
+ const dir = this.getSystemConfigDir();
394
+ // Before anything is opened, because this can reject: an update takes the user out of the
395
+ // service config and that account may have been deleted since. A descriptor opened first
396
+ // would be left open on the way out.
397
+ const { uid, gid } = await this.resolveUserIds(user || 'root');
398
+ // Runs as root over a directory the service user writes to, so a symlink in here is
399
+ // something they could have put there: following one would hand root's ownership of any
400
+ // path on the machine to that account, /etc/passwd included. The open refusing is how that
401
+ // is found out - a check followed by a chown on the same name is two different lookups.
402
+ let fd;
403
+ try {
404
+ fd = this.openNoFollow(dir, true);
405
+ }
406
+ catch {
407
+ throw new Error((0, texts_1.ERR_AGENT_DIR_REPLACED)(dir));
408
+ }
409
+ try {
410
+ // The directory itself is the one that has to land, so this is allowed to throw: without
411
+ // it the service cannot write, which is the failure the caller has to hear about
412
+ (0, node_fs_1.fchownSync)(fd, uid, gid);
413
+ }
414
+ finally {
415
+ (0, node_fs_1.closeSync)(fd);
416
+ }
417
+ // Direct children only. The one path component resolved here is the directory itself, and
418
+ // replacing that takes write access to /etc, which is root's alone - so lchown, which does
419
+ // not follow the last component, cannot be steered anywhere. A path below a subdirectory
420
+ // would resolve a component the service user does control: swapped for a symlink between
421
+ // this readdir and the call, it hands them root's ownership of whatever it points at. Node
422
+ // has no openat to walk down safely, and nothing under here needs one.
423
+ (0, node_fs_1.readdirSync)(dir, { withFileTypes: true }).forEach((entry) => {
424
+ const p = path_1.default.join(dir, entry.name);
425
+ if (entry.isDirectory()) {
426
+ // `update/` is the only one that ever turns up here, and swapBinaryToVersion removes
427
+ // and re-creates it as the service user before it writes anything into it
428
+ logger_1.default.error((0, texts_1.LOG_AGENT_RIGHTS_DIR_SKIPPED)(p));
429
+ return;
430
+ }
431
+ try {
432
+ (0, node_fs_1.lchownSync)(p, uid, gid);
433
+ }
434
+ catch (err) {
435
+ // Leaving one file behind is not worth undoing the handover for
436
+ logger_1.default.error(err);
437
+ }
438
+ });
439
+ }
440
+ // The modes below are applied where each file is written, so that only covers what this
441
+ // version wrote. This re-applies them to a whole directory, which is how an install made by
442
+ // an older version gets off 0777/0666. Only ever called directly after chownSystemDir, and
443
+ // never before it: on their own the tighter modes would lock out a service user who does not
444
+ // own these yet.
445
+ applySystemModes() {
446
+ const modes = [
447
+ [this.getSystemConfigDir(), 0o755],
448
+ // Ordinary users still read this one: it is how `agent status` and the tunnel commands
449
+ // find the install without sudo. They just cannot write it any more.
450
+ [this.getSystemConfigPath(), 0o644],
451
+ [this.getSystemStandalonePath(), 0o644],
452
+ // The agent's ssh host key - nothing but the agent process ever reads it
453
+ [path_1.default.join(this.getSystemConfigDir(), 'srv.json'), 0o600],
454
+ [this.getUpdateStatePath(), 0o600],
455
+ [this.getSystemBinaryPath(), 0o755],
456
+ [this.getSystemBinaryBackupPath(), 0o755],
457
+ [path_1.default.join(this.getSystemConfigDir(), 'cli.log'), 0o640],
458
+ [path_1.default.join(this.getSystemConfigDir(), 'cli.1.log'), 0o640],
459
+ ];
460
+ modes.forEach(([p, mode]) => {
461
+ if (!(0, node_fs_1.existsSync)(p))
462
+ return;
463
+ // Windows has no symlink to worry about here and no flag to refuse one with. It also has
464
+ // no caller: only the two posix classes apply the modes.
465
+ if ((0, utils_1.isWindows)()) {
466
+ try {
467
+ (0, node_fs_1.chmodSync)(p, mode);
468
+ }
469
+ catch (err) {
470
+ logger_1.default.error(err);
471
+ }
472
+ return;
473
+ }
474
+ // chmod follows symlinks and there is no portable lchmod, so a link left here by the
475
+ // service user - or by anyone, on a directory still at the old 0777 - would take this
476
+ // root process to a file of their choosing. Checking the name first does not help: the
477
+ // chmod is a second lookup, and the name can be a link by then. So the mode goes on the
478
+ // descriptor, and the open is what refuses a link in the first place.
479
+ let fd;
480
+ try {
481
+ fd = this.openNoFollow(p);
482
+ }
483
+ catch (err) {
484
+ logger_1.default.error((0, texts_1.ERR_AGENT_DIR_REPLACED)(p));
485
+ logger_1.default.error(err);
486
+ return;
487
+ }
488
+ try {
489
+ // Anything that is not a file or a directory was not put here by this cli, and its mode
490
+ // is not ours to set - a device node or a socket, on a directory still open to everyone
491
+ const st = (0, node_fs_1.fstatSync)(fd);
492
+ if (st.isFile() || st.isDirectory())
493
+ (0, node_fs_1.fchmodSync)(fd, mode);
494
+ }
495
+ catch (err) {
496
+ // A mode we cannot set is not worth failing an install over
497
+ logger_1.default.error(err);
498
+ }
499
+ finally {
500
+ (0, node_fs_1.closeSync)(fd);
501
+ }
502
+ });
503
+ }
504
+ ensureStandaloneConfigDir() {
505
+ const p = this.getStandaloneConfigDir();
506
+ (0, node_fs_1.mkdirSync)(p, { recursive: true });
507
+ try {
508
+ (0, node_fs_1.chmodSync)(p, 0o700);
509
+ }
510
+ catch {
511
+ // do nothing, a dir left by another user is still theirs to keep
512
+ }
513
+ }
514
+ // The directory the install this cli acts on writes to. Called where the old code called
515
+ // ensureSystemConfigDir: for a standalone install in the user directory that one would
516
+ // both fail without root and be the wrong place.
517
+ ensureActiveConfigDir() {
518
+ if (this.isUserOwnedInstall())
519
+ this.ensureStandaloneConfigDir();
520
+ else
521
+ this.ensureSystemConfigDir();
154
522
  }
155
523
  saveFile(p, c) {
156
524
  return new Promise((resolve, reject) => {
@@ -158,6 +526,10 @@ class AgentSystem {
158
526
  if (err)
159
527
  reject(err);
160
528
  else {
529
+ // The service config, and it carries the token in its arguments. Left at 0644
530
+ // deliberately: launchd and systemd have their own expectations of these files, and
531
+ // the token is readable off `ps` by every local user anyway - the fix for that is
532
+ // getting it out of the command line, not tightening this mode.
161
533
  (0, node_fs_1.chmod)(p, 0o644, (err) => {
162
534
  if (err)
163
535
  reject(err);
@@ -190,7 +562,7 @@ class AgentSystem {
190
562
  if (err)
191
563
  reject(err);
192
564
  else {
193
- (0, node_fs_1.chmod)(dest, 0o666, (err) => {
565
+ (0, node_fs_1.chmod)(dest, 0o600, (err) => {
194
566
  if (err)
195
567
  reject(err);
196
568
  else
@@ -284,7 +656,7 @@ class AgentSystem {
284
656
  encoding: 'utf-8',
285
657
  });
286
658
  try {
287
- (0, node_fs_1.chmodSync)(this.getUpdateStatePath(), 0o666);
659
+ (0, node_fs_1.chmodSync)(this.getUpdateStatePath(), 0o600);
288
660
  }
289
661
  catch {
290
662
  // do nothing
@@ -400,7 +772,7 @@ class AgentSystem {
400
772
  if (!(0, node_fs_1.existsSync)(staged)) {
401
773
  throw new Error(`Archive did not contain '${path_1.default.basename(target)}'`);
402
774
  }
403
- (0, node_fs_1.chmodSync)(staged, 0o777);
775
+ (0, node_fs_1.chmodSync)(staged, 0o755);
404
776
  const runs = await this.verifyBinary(staged, version);
405
777
  if (!runs)
406
778
  return false;
@@ -505,8 +877,8 @@ class AgentSystem {
505
877
  clearStandaloneProcConfig() {
506
878
  try {
507
879
  logger_1.default.info(texts_1.LOG_REMOVING_AGENT_PROC_ID);
508
- this.ensureSystemConfigDir();
509
- (0, node_fs_1.rmSync)(this.getSystemStandalonePath(), {
880
+ this.ensureActiveConfigDir();
881
+ (0, node_fs_1.rmSync)(this.getActivePidPath(), {
510
882
  force: true,
511
883
  });
512
884
  return true;
@@ -520,12 +892,12 @@ class AgentSystem {
520
892
  saveStandaloneProcConfig() {
521
893
  try {
522
894
  logger_1.default.info(texts_1.LOG_SAVING_AGENT_PROC_ID);
523
- this.ensureSystemConfigDir();
524
- const p = this.getSystemStandalonePath();
895
+ this.ensureActiveConfigDir();
896
+ const p = this.getActivePidPath();
525
897
  (0, node_fs_1.writeFileSync)(p, String(process.pid), {
526
898
  encoding: 'utf-8',
527
899
  });
528
- (0, node_fs_1.chmodSync)(p, 0o666);
900
+ (0, node_fs_1.chmodSync)(p, this.getPidFileMode());
529
901
  return true;
530
902
  }
531
903
  catch (err) {
@@ -546,8 +918,13 @@ class AgentSystem {
546
918
  saveSystemConfig(id, host, token, port, standalone, debug) {
547
919
  try {
548
920
  logger_1.default.info(texts_1.LOG_SAVING_AGENT_SYSTEM_CONFIG);
549
- this.ensureSystemConfigDir();
550
- (0, node_fs_1.writeFileSync)(this.getSystemConfigPath(), JSON.stringify({
921
+ const p = this.getConfigPathForWrite(standalone);
922
+ const inUserDir = p === this.getStandaloneConfigPath();
923
+ if (inUserDir)
924
+ this.ensureStandaloneConfigDir();
925
+ else
926
+ this.ensureSystemConfigDir();
927
+ (0, node_fs_1.writeFileSync)(p, JSON.stringify({
551
928
  id,
552
929
  host,
553
930
  token,
@@ -557,7 +934,9 @@ class AgentSystem {
557
934
  }), {
558
935
  encoding: 'utf-8',
559
936
  });
560
- (0, node_fs_1.chmodSync)(this.getSystemConfigPath(), 0o666);
937
+ // Taken from where the file actually landed rather than from getActiveFileMode: this
938
+ // is the one write that can create the file the resolution keys on
939
+ (0, node_fs_1.chmodSync)(p, inUserDir ? 0o600 : 0o644);
561
940
  return true;
562
941
  }
563
942
  catch (err) {
@@ -581,7 +960,7 @@ class AgentSystem {
581
960
  encoding: 'utf-8',
582
961
  });
583
962
  try {
584
- (0, node_fs_1.chmodSync)(this.getNewAgentConfigPath(), 0o666);
963
+ (0, node_fs_1.chmodSync)(this.getNewAgentConfigPath(), 0o600);
585
964
  }
586
965
  catch {
587
966
  // do nothing, file can already have proper mode
@@ -29,9 +29,6 @@ class AgentWindows extends system_1.default {
29
29
  getSystemConfigPath() {
30
30
  return path_1.default.join(this.getSystemConfigDir(), 'agent.json');
31
31
  }
32
- getSystemStandalonePath() {
33
- return path_1.default.join(this.getSystemConfigDir(), 'pid.lock');
34
- }
35
32
  getNssmArchivePath() {
36
33
  return path_1.default.join(this.getSystemConfigDir(), 'nssm.zip');
37
34
  }
@@ -53,10 +50,10 @@ class AgentWindows extends system_1.default {
53
50
  getSystemBinaryPath() {
54
51
  return path_1.default.join(this.getSystemConfigDir(), 'bdy.exe');
55
52
  }
56
- clearSystemFiles() {
57
- (0, fs_1.rmSync)(this.getSystemBinaryPath(), { force: true });
58
- (0, fs_1.rmSync)(this.getSystemConfigPath(), { force: true });
59
- this.clearUpdateFiles();
53
+ // The service half only: nssm is what runs the service, so an `--app` uninstall has no
54
+ // business removing it
55
+ clearServiceFiles() {
56
+ super.clearServiceFiles();
60
57
  (0, fs_1.rmSync)(path_1.default.join(this.getSystemConfigDir(), 'nssm'), {
61
58
  force: true,
62
59
  recursive: true,
@@ -11,8 +11,8 @@ commandAgentDebug.argument('<on/off>');
11
11
  commandAgentDebug.action(async (debug) => {
12
12
  const AgentManager = require('../../agent/manager').default;
13
13
  const ApiAgent = require('../../tunnel/api/agent').default;
14
- const hasAdminRights = await AgentManager.system.hasAdminRights();
15
- if (!hasAdminRights) {
14
+ const hasRights = await AgentManager.system.hasRightsToManage();
15
+ if (!hasRights) {
16
16
  output_1.default.exitError(texts_1.ERR_AGENT_ADMIN_RIGHTS);
17
17
  }
18
18
  if (!commandAgentDebug.agentInstalled) {
@@ -9,6 +9,7 @@ const texts_1 = require("../../texts");
9
9
  const cfg_1 = __importDefault(require("../../tunnel/cfg"));
10
10
  const utils_1 = require("../../utils");
11
11
  const logger_1 = __importDefault(require("../../logger"));
12
+ const node_net_1 = require("node:net");
12
13
  const removeAllAndExit = async (txt, id, host, token) => {
13
14
  const AgentManager = require('../../agent/manager').default;
14
15
  const ApiBuddy = require('../../tunnel/api/buddy').default;
@@ -25,7 +26,9 @@ const removeAllAndExit = async (txt, id, host, token) => {
25
26
  // do nothing
26
27
  }
27
28
  try {
28
- AgentManager.system.clearSystemFiles();
29
+ // Named rather than resolved: this only ever unmakes the service install started above, and
30
+ // the account running it can have an `--app` install of its own that a resolver would pick
31
+ AgentManager.system.clearServiceFiles();
29
32
  }
30
33
  catch {
31
34
  // do nothing
@@ -36,14 +39,19 @@ const removeAllAndExit = async (txt, id, host, token) => {
36
39
  catch {
37
40
  // do nothing
38
41
  }
39
- try {
40
- AgentManager.system.clearNewAgentFiles();
41
- }
42
- catch {
43
- // do nothing
44
- }
45
42
  output_1.default.exitError(txt);
46
43
  };
44
+ // A standalone install in another user's directory cannot be seen from here, so the config
45
+ // check below no longer proves the machine is free - see agent/system.ts isUserOwnedInstall.
46
+ // The port is what two agents actually compete for, and this binds it the way the agent does
47
+ // (manager.ts listens without a host), so the answer is the one that will matter on start.
48
+ const isPortFree = (port) => new Promise((resolve) => {
49
+ const srv = (0, node_net_1.createServer)();
50
+ srv.once('error', () => resolve(false));
51
+ srv.listen(port, () => {
52
+ srv.close(() => resolve(true));
53
+ });
54
+ });
47
55
  const prepareTags = (tags) => {
48
56
  const validTags = [];
49
57
  if (tags) {
@@ -235,14 +243,33 @@ commandAgentInstall.option('--agent-note <note>', texts_1.OPT_COMMAND_AGENT_NOTE
235
243
  commandAgentInstall.action(async (options) => {
236
244
  const AgentManager = require('../../agent/manager').default;
237
245
  options.start = options.start === 'true' || options.start === true;
238
- const hasAdminRights = await AgentManager.system.hasAdminRights();
239
- if (!hasAdminRights) {
240
- output_1.default.exitError(texts_1.ERR_AGENT_ADMIN_RIGHTS);
246
+ // `--app` installs no service: the config goes to this user's own directory and the agent
247
+ // runs as this user, so there is nothing left in the mode that needs root. A service
248
+ // install writes to LaunchDaemons/systemd/ProgramData and still does.
249
+ if (!options.app) {
250
+ const hasAdminRights = await AgentManager.system.hasAdminRights();
251
+ if (!hasAdminRights) {
252
+ output_1.default.exitError(texts_1.ERR_AGENT_ADMIN_RIGHTS);
253
+ }
241
254
  }
242
255
  const hasSystemConfig = AgentManager.system.hasSystemConfig();
243
256
  if (hasSystemConfig) {
244
257
  output_1.default.exitError(texts_1.TXT_AGENT_ALREADY_INSTALLED);
245
258
  }
259
+ // One agent per machine. The check above only sees the system config and this account's own
260
+ // standalone one - an `--app` install in another home is invisible from there, and a service
261
+ // installed on top of it would fight it for the port and the agent id.
262
+ if (!options.app) {
263
+ const apps = AgentManager.system.findAppInstalls();
264
+ if (apps.length) {
265
+ output_1.default.exitError((0, texts_1.ERR_AGENT_APP_INSTALL_PRESENT)(apps[0]));
266
+ }
267
+ }
268
+ const port = input_1.default.port(options.port);
269
+ const free = await isPortFree(port);
270
+ if (!free) {
271
+ output_1.default.exitError((0, texts_1.ERR_AGENT_PORT_TAKEN)(port));
272
+ }
246
273
  if (options.app) {
247
274
  await installApp(options);
248
275
  }