relmio 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,514 @@
1
+ import { randomBytes as cryptoRandomBytes } from "node:crypto";
2
+
3
+ import {
4
+ ASSISTANT_MARKER_PATH,
5
+ ASSISTANT_PRECHECK_COMMAND,
6
+ ASSISTANT_ROOT,
7
+ ASSISTANT_ROOT_MARKER_PATH,
8
+ assertAssistantOnlyCommands,
9
+ createAssistantDeploymentCommands,
10
+ createAssistantExactResourceAttestationCommands,
11
+ createAssistantInstallation,
12
+ createAssistantNetworkCollisionCommand,
13
+ createAssistantOwnershipAttestationCommands,
14
+ createAssistantVerificationCommands,
15
+ getAssistantContainerNames,
16
+ getAssistantManagedResourceNames,
17
+ getAssistantServiceNames,
18
+ parseAssistantPrecheck,
19
+ serializeAssistantMarker,
20
+ validateAssistantSearxngSelection,
21
+ } from "../domain/assistant.js";
22
+ import { SHARED_ROOT_MARKER_CONTENT } from "../domain/safety.js";
23
+ import {
24
+ createAssistantComposeFile,
25
+ createAssistantEnv,
26
+ createAssistantSecrets,
27
+ createSearxngSettings,
28
+ } from "../domain/assistant-templates.js";
29
+
30
+ const CONTAINER_ID_PATTERN = /^[a-f0-9]{12,64}$/iu;
31
+
32
+ function getExpectedRunningServices(installation) {
33
+ return new Set(
34
+ getAssistantServiceNames(installation).filter(
35
+ (service) => service !== "relmio-sandbox-certs",
36
+ ),
37
+ );
38
+ }
39
+
40
+ function parseJsonLines(output, label) {
41
+ if (typeof output !== "string" || output.trim() === "") {
42
+ throw new Error(label + " could not be verified.");
43
+ }
44
+ try {
45
+ const parsed = JSON.parse(output);
46
+ return Array.isArray(parsed) ? parsed : [parsed];
47
+ } catch {
48
+ try {
49
+ return output.trim().split("\n").map((line) => JSON.parse(line));
50
+ } catch {
51
+ throw new Error(label + " could not be verified.");
52
+ }
53
+ }
54
+ }
55
+
56
+ function parseSandboxHealth(output) {
57
+ const values = parseJsonLines(output, "The sandbox API health");
58
+ if (values.length !== 1 || values[0]?.status !== "ok") {
59
+ throw new Error("The sandbox API health could not be verified.");
60
+ }
61
+ }
62
+
63
+ function verifyRunningServices(output, installation) {
64
+ if (typeof output !== "string") {
65
+ throw new Error("The companion service status could not be verified.");
66
+ }
67
+ const expectedRunningServices = getExpectedRunningServices(installation);
68
+ const running = new Set(output.split("\n").map((value) => value.trim()).filter(Boolean));
69
+ if (
70
+ running.size !== expectedRunningServices.size ||
71
+ [...expectedRunningServices].some((service) => !running.has(service))
72
+ ) {
73
+ throw new Error("The expected AI Assistant companion services are not running.");
74
+ }
75
+ }
76
+
77
+ function hasPublishedHostPort(output, installation) {
78
+ const services = parseJsonLines(output, "The published-port state");
79
+ const expectedRunningServices = getExpectedRunningServices(installation);
80
+ if (services.length !== expectedRunningServices.size) {
81
+ throw new Error("The published-port state could not be verified.");
82
+ }
83
+ const seen = new Set();
84
+ for (const service of services) {
85
+ if (
86
+ !service ||
87
+ typeof service.Service !== "string" ||
88
+ !expectedRunningServices.has(service.Service) ||
89
+ seen.has(service.Service) ||
90
+ (service.Publishers !== null && !Array.isArray(service.Publishers))
91
+ ) {
92
+ throw new Error("The published-port state could not be verified.");
93
+ }
94
+ seen.add(service.Service);
95
+ for (const publisher of service.Publishers ?? []) {
96
+ if (
97
+ !publisher ||
98
+ !Number.isInteger(publisher.PublishedPort) ||
99
+ publisher.PublishedPort < 0 ||
100
+ typeof publisher.URL !== "string"
101
+ ) {
102
+ throw new Error("The published-port state could not be verified.");
103
+ }
104
+ if (publisher.PublishedPort > 0 || publisher.URL.trim() !== "") {
105
+ return true;
106
+ }
107
+ }
108
+ }
109
+ return false;
110
+ }
111
+
112
+ async function runOrThrow(remote, command, label) {
113
+ const result = await remote.exec(command);
114
+ if (result.code !== 0) {
115
+ throw new Error(label + " failed. The existing n8n deployment was not changed.");
116
+ }
117
+ return result;
118
+ }
119
+
120
+ function attestOutput(output, installation, label) {
121
+ if (typeof output !== "string" || output.length > 16 * 1024) {
122
+ throw new Error("The AI Assistant " + label + " ownership could not be verified.");
123
+ }
124
+ let count = 0;
125
+ for (const line of output.split("\n").map((value) => value.trim()).filter(Boolean)) {
126
+ if (line !== installation.installId + "|true") {
127
+ throw new Error("The AI Assistant " + label + " ownership could not be verified.");
128
+ }
129
+ count += 1;
130
+ }
131
+ return count;
132
+ }
133
+
134
+ function attestExactResourceOutput(
135
+ output,
136
+ installation,
137
+ label,
138
+ policy,
139
+ { legacyOwnedContainerCount = 0 } = {},
140
+ ) {
141
+ if (typeof output !== "string" || output.length > 16 * 1024) {
142
+ throw new Error("The AI Assistant exact " + label + " ownership could not be verified.");
143
+ }
144
+ const names = getAssistantManagedResourceNames(installation);
145
+ const expectedNames = new Set(
146
+ label === "containers" ? names.containers : [names[label]],
147
+ );
148
+ const seen = new Set();
149
+ const enablingSearxng = policy === "enable-searxng";
150
+ const searxngContainer = getAssistantContainerNames(installation).searxng;
151
+ for (const line of output.split("\n").map((value) => value.trim()).filter(Boolean)) {
152
+ const [name, installId, managed, ...extra] = line.split("|");
153
+ if (
154
+ extra.length > 0 ||
155
+ !expectedNames.has(name) ||
156
+ seen.has(name) ||
157
+ typeof installId !== "string" ||
158
+ typeof managed !== "string"
159
+ ) {
160
+ throw new Error("The AI Assistant exact " + label + " ownership could not be verified.");
161
+ }
162
+ seen.add(name);
163
+ if (policy === "absent") {
164
+ throw new Error("A predictable AI Assistant " + label + " resource name is already occupied.");
165
+ }
166
+ if (enablingSearxng && label === "containers" && name === searxngContainer) {
167
+ throw new Error("The optional AI Assistant SearXNG container is already occupied.");
168
+ }
169
+ if (installId !== installation.installId || managed !== "true") {
170
+ throw new Error("The AI Assistant exact " + label + " ownership could not be verified.");
171
+ }
172
+ }
173
+ if (enablingSearxng) {
174
+ if (
175
+ label === "containers" &&
176
+ seen.size === 0 &&
177
+ legacyOwnedContainerCount >= getAssistantServiceNames({
178
+ ...installation,
179
+ includeSearxng: false,
180
+ }).length
181
+ ) {
182
+ return;
183
+ }
184
+ const requiredOwnedNames = label === "containers"
185
+ ? names.containers.filter((name) => name !== searxngContainer)
186
+ : [names[label]];
187
+ if (requiredOwnedNames.some((name) => !seen.has(name))) {
188
+ throw new Error("The AI Assistant exact " + label + " ownership could not be verified.");
189
+ }
190
+ }
191
+ }
192
+
193
+ async function attestAssistantOwnership(remote, installation, { exactResourcePolicy = "owned" } = {}) {
194
+ const commands = createAssistantOwnershipAttestationCommands({ installation });
195
+ const ownershipCounts = {};
196
+ for (const [label, command] of Object.entries(commands)) {
197
+ const result = await remote.exec(command);
198
+ if (result.code !== 0) {
199
+ throw new Error("The AI Assistant " + label + " ownership could not be verified.");
200
+ }
201
+ ownershipCounts[label] = attestOutput(result.stdout, installation, label);
202
+ }
203
+ const exactCommands = createAssistantExactResourceAttestationCommands({ installation });
204
+ for (const [label, command] of Object.entries(exactCommands)) {
205
+ const result = await remote.exec(command);
206
+ if (result.code !== 0) {
207
+ throw new Error("The AI Assistant exact " + label + " ownership could not be verified.");
208
+ }
209
+ attestExactResourceOutput(result.stdout, installation, label, exactResourcePolicy, {
210
+ legacyOwnedContainerCount: ownershipCounts.containers,
211
+ });
212
+ }
213
+ }
214
+
215
+ function parseNetworkAliases(output, installation) {
216
+ if (typeof output !== "string" || output.length > 64 * 1024) {
217
+ throw new Error("The selected network alias state could not be verified.");
218
+ }
219
+ const requestedAliases = new Set([
220
+ installation.sandboxAlias,
221
+ ...(installation.includeSearxng ? [installation.searxngAlias] : []),
222
+ ]);
223
+ for (const line of output.split("\n").map((value) => value.trim()).filter(Boolean)) {
224
+ const [containerId, installId, aliases, ...extra] = line.split("|");
225
+ if (
226
+ extra.length > 0 ||
227
+ !CONTAINER_ID_PATTERN.test(containerId ?? "") ||
228
+ typeof installId !== "string" ||
229
+ typeof aliases !== "string"
230
+ ) {
231
+ throw new Error("The selected network alias state could not be verified.");
232
+ }
233
+ const collidingAlias = aliases.split(",").some((alias) => requestedAliases.has(alias));
234
+ if (collidingAlias && installId !== installation.installId) {
235
+ throw new Error("A requested AI Assistant network alias is already attached to a foreign container.");
236
+ }
237
+ }
238
+ }
239
+
240
+ async function verifyNetworkAliases(remote, networkName, installation) {
241
+ const command = createAssistantNetworkCollisionCommand({
242
+ networkName,
243
+ installation,
244
+ });
245
+ const result = await remote.exec(command);
246
+ if (result.code !== 0) {
247
+ throw new Error("The selected network alias state could not be verified.");
248
+ }
249
+ parseNetworkAliases(result.stdout, installation);
250
+ }
251
+
252
+ async function failAfterStart({
253
+ remote,
254
+ installation,
255
+ cleanupCommand,
256
+ cleanupScope,
257
+ reason,
258
+ cleanupState,
259
+ }) {
260
+ const searxngOnly = cleanupScope === "searxng";
261
+ if (cleanupState.attempted) {
262
+ throw new Error(
263
+ reason + (searxngOnly
264
+ ? " Optional SearXNG cleanup was already attempted; do not use the AI Assistant companion."
265
+ : " Cleanup was already attempted; do not use the AI Assistant companion."),
266
+ );
267
+ }
268
+ cleanupState.attempted = true;
269
+ try {
270
+ await attestAssistantOwnership(remote, installation);
271
+ } catch {
272
+ throw new Error(
273
+ reason + (searxngOnly
274
+ ? " Optional SearXNG ownership could not be confirmed, so cleanup was not attempted. Do not use the AI Assistant companion until an administrator verifies its state."
275
+ : " Companion ownership could not be confirmed, so cleanup was not attempted. Do not use the AI Assistant companion until its ownership is inspected."),
276
+ );
277
+ }
278
+
279
+ let cleanupSucceeded = false;
280
+ try {
281
+ cleanupSucceeded = (await remote.exec(cleanupCommand)).code === 0;
282
+ } catch {
283
+ cleanupSucceeded = false;
284
+ }
285
+ if (!cleanupSucceeded) {
286
+ const cleanupMessage = searxngOnly
287
+ ? "Automatic optional SearXNG cleanup could not be confirmed. Do not use the AI Assistant companion until an administrator verifies its state."
288
+ : "Automatic cleanup could not be confirmed. Do not use the AI Assistant companion until an administrator confirms its removal.";
289
+ throw Object.assign(
290
+ new Error(
291
+ reason + " " + cleanupMessage,
292
+ ),
293
+ {
294
+ safeMessage: cleanupMessage,
295
+ },
296
+ );
297
+ }
298
+ if (searxngOnly) {
299
+ throw new Error(
300
+ reason + " The optional SearXNG service was removed. The existing sandbox remains; do not use the AI Assistant companion until an administrator verifies its state.",
301
+ );
302
+ }
303
+ throw new Error(
304
+ reason + " The ownership-attested AI Assistant companion project was removed; the existing n8n deployment was not changed.",
305
+ );
306
+ }
307
+
308
+ export async function installAssistant({
309
+ remote,
310
+ networkName,
311
+ confirmed,
312
+ includeSearxng,
313
+ randomBytes = cryptoRandomBytes,
314
+ }) {
315
+ if (confirmed !== true) {
316
+ throw new Error("Confirm the AI Assistant companion deployment before installing.");
317
+ }
318
+ const selectedSearxng = validateAssistantSearxngSelection(includeSearxng);
319
+
320
+ const precheck = await remote.exec(ASSISTANT_PRECHECK_COMMAND);
321
+ if (precheck.code === 42) {
322
+ throw new Error("The AI Assistant directory or shared root already exists and is unmanaged. Nothing was overwritten.");
323
+ }
324
+ if (precheck.code === 43) {
325
+ throw new Error("The AI Assistant directory or shared root is unsafe or symlinked. Nothing was overwritten.");
326
+ }
327
+ if (precheck.code !== 0) {
328
+ throw new Error("The VPS AI Assistant install-directory check failed.");
329
+ }
330
+
331
+ const precheckResult = parseAssistantPrecheck(precheck.stdout);
332
+ const previousInstallation = precheckResult.installation;
333
+ if (
334
+ previousInstallation?.includeSearxng === true &&
335
+ selectedSearxng === false
336
+ ) {
337
+ throw new Error(
338
+ "Disabling previously managed SearXNG would remove a companion service. Use a separately authorized cleanup path instead.",
339
+ );
340
+ }
341
+ const enablingSearxng =
342
+ previousInstallation?.includeSearxng === false && selectedSearxng === true;
343
+ const installation = previousInstallation
344
+ ? { ...previousInstallation, includeSearxng: selectedSearxng }
345
+ : createAssistantInstallation({ randomBytes, includeSearxng: selectedSearxng });
346
+ const deploymentMode = precheckResult.state === "managed" ? "updated" : "installed";
347
+ const startupScope = enablingSearxng ? "searxng" : "all";
348
+ const cleanupScope = enablingSearxng ? "searxng" : "project";
349
+ const deploymentCommands = createAssistantDeploymentCommands({ installation, startupScope });
350
+ const verification = createAssistantVerificationCommands({ installation, cleanupScope });
351
+ const exactResourcePolicy = deploymentMode === "installed"
352
+ ? "absent"
353
+ : enablingSearxng
354
+ ? "enable-searxng"
355
+ : "owned";
356
+ const collisionCommand = createAssistantNetworkCollisionCommand({
357
+ networkName,
358
+ installation,
359
+ });
360
+ assertAssistantOnlyCommands({
361
+ commands: [
362
+ ASSISTANT_PRECHECK_COMMAND,
363
+ collisionCommand,
364
+ ...Object.values(createAssistantOwnershipAttestationCommands({ installation })),
365
+ ...Object.values(createAssistantExactResourceAttestationCommands({ installation })),
366
+ ...deploymentCommands,
367
+ ...Object.values(verification),
368
+ ],
369
+ installation,
370
+ networkName,
371
+ startupScope,
372
+ cleanupScope,
373
+ });
374
+
375
+ await verifyNetworkAliases(remote, networkName, installation);
376
+ await attestAssistantOwnership(remote, installation, { exactResourcePolicy });
377
+
378
+ const composeFile = createAssistantComposeFile({ networkName, installation });
379
+ const settingsFile = installation.includeSearxng ? createSearxngSettings() : null;
380
+ const secrets = enablingSearxng
381
+ ? null
382
+ : createAssistantSecrets({ randomBytes, includeSearxng: installation.includeSearxng });
383
+ const envFile = secrets
384
+ ? createAssistantEnv(secrets, { includeSearxng: installation.includeSearxng })
385
+ : null;
386
+
387
+ await runOrThrow(remote, deploymentCommands[0], "AI Assistant shared root creation");
388
+ await runOrThrow(remote, deploymentCommands[1], "AI Assistant directory creation");
389
+ await remote.upload(ASSISTANT_ROOT_MARKER_PATH, SHARED_ROOT_MARKER_CONTENT, 0o600);
390
+ if (!enablingSearxng) {
391
+ await remote.upload(ASSISTANT_MARKER_PATH, serializeAssistantMarker(installation), 0o600);
392
+ }
393
+ await remote.upload(ASSISTANT_ROOT + "/docker-compose.yml", composeFile, 0o644);
394
+ if (settingsFile) {
395
+ await remote.upload(ASSISTANT_ROOT + "/searxng-settings.yml", settingsFile, 0o644);
396
+ }
397
+ if (envFile) {
398
+ await remote.upload(ASSISTANT_ROOT + "/.env", envFile, 0o600);
399
+ }
400
+ await runOrThrow(remote, deploymentCommands[2], "AI Assistant private file permissions");
401
+ await runOrThrow(remote, deploymentCommands[3], "AI Assistant public file permissions");
402
+ await runOrThrow(remote, deploymentCommands[4], "AI Assistant private file mode verification");
403
+ await runOrThrow(remote, deploymentCommands[5], "AI Assistant public file mode verification");
404
+ await runOrThrow(remote, deploymentCommands[6], "AI Assistant Compose validation");
405
+ await attestAssistantOwnership(remote, installation, { exactResourcePolicy });
406
+
407
+ const cleanupState = { attempted: false };
408
+ try {
409
+ await runOrThrow(remote, deploymentCommands[7], "AI Assistant companion startup");
410
+ } catch {
411
+ await failAfterStart({
412
+ remote,
413
+ installation,
414
+ cleanupCommand: verification.cleanup,
415
+ cleanupScope,
416
+ reason: "The AI Assistant companion startup failed.",
417
+ cleanupState,
418
+ });
419
+ }
420
+
421
+ try {
422
+ const health = await runOrThrow(
423
+ remote,
424
+ verification.health,
425
+ "Sandbox API health check",
426
+ );
427
+ parseSandboxHealth(health.stdout);
428
+ } catch {
429
+ await failAfterStart({
430
+ remote,
431
+ installation,
432
+ cleanupCommand: verification.cleanup,
433
+ cleanupScope,
434
+ reason: "The sandbox API health check failed.",
435
+ cleanupState,
436
+ });
437
+ }
438
+
439
+ try {
440
+ const running = await runOrThrow(
441
+ remote,
442
+ verification.runningServices,
443
+ "AI Assistant companion status check",
444
+ );
445
+ verifyRunningServices(running.stdout, installation);
446
+ } catch {
447
+ await failAfterStart({
448
+ remote,
449
+ installation,
450
+ cleanupCommand: verification.cleanup,
451
+ cleanupScope,
452
+ reason: "The AI Assistant companion service check failed.",
453
+ cleanupState,
454
+ });
455
+ }
456
+
457
+ let publishedHostPort;
458
+ try {
459
+ const publication = await remote.exec(verification.publicationState);
460
+ if (publication.code !== 0) {
461
+ throw new Error("The published-port safety check failed.");
462
+ }
463
+ publishedHostPort = hasPublishedHostPort(publication.stdout, installation);
464
+ } catch {
465
+ await failAfterStart({
466
+ remote,
467
+ installation,
468
+ cleanupCommand: verification.cleanup,
469
+ cleanupScope,
470
+ reason: "The published-port safety check failed.",
471
+ cleanupState,
472
+ });
473
+ }
474
+ if (publishedHostPort) {
475
+ await failAfterStart({
476
+ remote,
477
+ installation,
478
+ cleanupCommand: verification.cleanup,
479
+ cleanupScope,
480
+ reason: "Safety check failed: the AI Assistant companion unexpectedly published a host port.",
481
+ cleanupState,
482
+ });
483
+ }
484
+
485
+ if (enablingSearxng) {
486
+ try {
487
+ await remote.upload(ASSISTANT_MARKER_PATH, serializeAssistantMarker(installation), 0o600);
488
+ await runOrThrow(remote, deploymentCommands[2], "AI Assistant private file permissions");
489
+ await runOrThrow(remote, deploymentCommands[4], "AI Assistant private file mode verification");
490
+ } catch {
491
+ await failAfterStart({
492
+ remote,
493
+ installation,
494
+ cleanupCommand: verification.cleanup,
495
+ cleanupScope,
496
+ reason: "The AI Assistant SearXNG marker update failed.",
497
+ cleanupState,
498
+ });
499
+ }
500
+ }
501
+
502
+ return {
503
+ sandboxUrl: "http://" + installation.sandboxAlias + ":8080",
504
+ sandboxApiKey: secrets?.sandboxApiKey ?? null,
505
+ includeSearxng: installation.includeSearxng,
506
+ webSearch: installation.includeSearxng ? "enabled" : "disabled",
507
+ ...(installation.includeSearxng
508
+ ? { searxngUrl: "http://" + installation.searxngAlias + ":8080" }
509
+ : {}),
510
+ modelProvider: "OpenAI",
511
+ modelRecommendation: "preserve-current-supported-selection",
512
+ deploymentMode,
513
+ };
514
+ }
@@ -76,9 +76,35 @@ export function createInspectNetworksCommand(containerName) {
76
76
  return `docker inspect ${safeName} --format '{{range $name, $_ := .NetworkSettings.Networks}}{{println $name}}{{end}}'`;
77
77
  }
78
78
 
79
+ export function createInspectN8nEnabledModulesCommand(containerName) {
80
+ const safeName = validateDockerName(containerName);
81
+ return `docker inspect ${safeName} --format '{{range .Config.Env}}{{if eq (index (split . "=") 0) "N8N_ENABLED_MODULES"}}configured|{{println (join (slice (split . "=") 1) "=")}}{{end}}{{end}}'`;
82
+ }
83
+
84
+ export function parseN8nEnabledModulesOutput(output) {
85
+ if (typeof output !== "string" || output.length > 4 * 1024) {
86
+ throw new Error("The n8n AI Assistant prerequisite could not be verified.");
87
+ }
88
+ const lines = output.trim().split("\n").filter(Boolean);
89
+ if (lines.length === 0) return { status: "missing" };
90
+ if (lines.length !== 1 || !lines[0].startsWith("configured|")) {
91
+ throw new Error("The n8n AI Assistant prerequisite could not be verified.");
92
+ }
93
+ const modules = lines[0]
94
+ .slice("configured|".length)
95
+ .split(",")
96
+ .map((entry) => entry.trim())
97
+ .filter(Boolean);
98
+ return { status: modules.includes("instance-ai") ? "enabled" : "configured" };
99
+ }
100
+
79
101
  export async function discoverNetworks(remote, containerName) {
80
102
  const command = createInspectNetworksCommand(containerName);
81
- const output = await runReadOnly(remote, command, "Docker network discovery");
103
+ const enabledModulesCommand = createInspectN8nEnabledModulesCommand(containerName);
104
+ const [output, enabledModulesOutput] = await Promise.all([
105
+ runReadOnly(remote, command, "Docker network discovery"),
106
+ runReadOnly(remote, enabledModulesCommand, "n8n AI Assistant prerequisite discovery"),
107
+ ]);
82
108
  const networks = [
83
109
  ...new Set(
84
110
  output
@@ -92,5 +118,6 @@ export async function discoverNetworks(remote, containerName) {
92
118
  return {
93
119
  networks,
94
120
  recommended: networks.includes("proxy") ? "proxy" : (networks[0] ?? null),
121
+ instanceAi: parseN8nEnabledModulesOutput(enabledModulesOutput),
95
122
  };
96
123
  }
@@ -2,6 +2,9 @@ import {
2
2
  INSTALL_ROOT,
3
3
  MANAGED_MARKER_PATH,
4
4
  PRECHECK_COMMAND,
5
+ SHARED_ROOT_MARKER_CONTENT,
6
+ SHARED_ROOT_MARKER_PATH,
7
+ SIDECAR_MARKER_CONTENT,
5
8
  assertSidecarOnlyCommands,
6
9
  createDeploymentCommands,
7
10
  createVerificationCommands,
@@ -13,8 +16,6 @@ import {
13
16
  } from "../domain/templates.js";
14
17
 
15
18
  const MAX_AUTH_FILE_BYTES = 128 * 1024;
16
- const MARKER_CONTENT = "Managed by n8n-openai-oauth-setup.\n";
17
-
18
19
  function validateAuthContents(contents) {
19
20
  if (!Buffer.isBuffer(contents)) {
20
21
  throw new TypeError("The OAuth credential file is invalid.");
@@ -112,8 +113,14 @@ async function failPublicationSafetyCheck(remote, cleanupCommand, reason) {
112
113
  cleanupSucceeded = false;
113
114
  }
114
115
  if (!cleanupSucceeded) {
115
- throw new Error(
116
- `${reason} Automatic cleanup could not be confirmed. Do not use the sidecar until it is removed from /docker/n8n-openai-oauth.`,
116
+ throw Object.assign(
117
+ new Error(
118
+ `${reason} Automatic cleanup could not be confirmed. Do not use the sidecar until it is removed from /docker/n8n-openai-oauth.`,
119
+ ),
120
+ {
121
+ safeMessage:
122
+ "Automatic cleanup could not be confirmed. Do not use the sidecar until an administrator confirms its removal.",
123
+ },
117
124
  );
118
125
  }
119
126
  throw new Error(
@@ -162,7 +169,8 @@ export async function installSidecar({
162
169
  await runOrThrow(remote, deploymentCommands[0], "Sidecar directory creation");
163
170
  await runOrThrow(remote, deploymentCommands[1], "Auth directory creation");
164
171
 
165
- await remote.upload(MANAGED_MARKER_PATH, MARKER_CONTENT, 0o644);
172
+ await remote.upload(SHARED_ROOT_MARKER_PATH, SHARED_ROOT_MARKER_CONTENT, 0o600);
173
+ await remote.upload(MANAGED_MARKER_PATH, SIDECAR_MARKER_CONTENT, 0o644);
166
174
  await remote.upload(`${INSTALL_ROOT}/Dockerfile`, dockerfile, 0o644);
167
175
  await remote.upload(
168
176
  `${INSTALL_ROOT}/docker-compose.yml`,