@addai/node 0.18.0 → 0.19.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.
@@ -64,6 +64,7 @@ const docker_1 = require("./desktop/docker");
64
64
  const manager_1 = require("./desktop/manager");
65
65
  const creds_1 = require("./desktop/creds");
66
66
  const install_engine_1 = require("./desktop/install-engine");
67
+ const engine_1 = require("./desktop/engine");
67
68
  const COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
68
69
  /** Own version, read the same way index.ts does. Resolved here rather than
69
70
  * imported from index.ts, which already imports this module. */
@@ -692,7 +693,11 @@ async function runInstallEngine(cmd) {
692
693
  };
693
694
  await update(cmd.id, 'running', { log });
694
695
  try {
695
- await (0, install_engine_1.installEngine)(onLog, async () => {
696
+ // Installed but unreachable is a permissions problem, not a missing one -
697
+ // the same button must repair rather than reinstall.
698
+ const before = await (0, engine_1.detectEngine)();
699
+ const blocked = !!before && !before.running;
700
+ const outcome = await (0, install_engine_1.installEngine)(onLog, async () => {
696
701
  // Park the command and let Studio collect it, exactly as a harness
697
702
  // login collects a paste-back code.
698
703
  await update(cmd.id, 'awaiting_input', { needs: 'sudo_password', log });
@@ -720,8 +725,19 @@ async function runInstallEngine(cmd) {
720
725
  await new Promise(r => setTimeout(r, INPUT_POLL_MS));
721
726
  }
722
727
  return null;
723
- });
728
+ }, blocked);
724
729
  (0, docker_1.resetProviderCache)();
730
+ if (outcome.needsRestart) {
731
+ // Group membership only applies to new processes, so this daemon still
732
+ // cannot reach the socket. Say so plainly and bounce it.
733
+ onLog('\nPermissions fixed. Restarting this machine\'s node so it picks up the new group…\n');
734
+ await update(cmd.id, 'completed', { log, restarting: true });
735
+ await (0, heartbeat_1.beat)();
736
+ if (restartHook) {
737
+ await restartHook({ commandId: cmd.id, version: VERSION, restartOnly: true });
738
+ }
739
+ return;
740
+ }
725
741
  const engine = await (0, docker_1.getProvider)(true);
726
742
  if (!engine) {
727
743
  await update(cmd.id, 'failed', { log }, 'Installed, but no engine is answering yet. On macOS, Docker Desktop has to be opened once before its daemon runs.');
@@ -8,6 +8,16 @@ export interface InstallPlan {
8
8
  }
9
9
  /** Pure, so the choice is testable on any machine. */
10
10
  export declare function installPlan(platform: NodeJS.Platform, isRoot: boolean): InstallPlan;
11
+ /** Docker is there but this user cannot reach its socket. The fix is group
12
+ * membership, not another install — and it needs root, so it takes the same
13
+ * password round trip. */
14
+ export declare function repairPlan(user: string): {
15
+ inner: string;
16
+ manual: string;
17
+ };
18
+ /** Group membership is fixed for FUTURE processes only, so the daemon that
19
+ * asked for the fix still cannot reach the socket until it restarts. */
20
+ export declare const REPAIR_NEEDS_RESTART = true;
11
21
  export declare function runningAsRoot(): boolean;
12
22
  /** Build the argv for a privileged install. The password NEVER goes in argv —
13
23
  * argv is world-readable through /proc on Linux, so anyone with a shell on
@@ -17,4 +27,9 @@ export declare function sudoArgv(inner: string): {
17
27
  file: string;
18
28
  args: string[];
19
29
  };
20
- export declare function installEngine(onLog: (s: string) => void, askPassword?: () => Promise<string | null>): Promise<void>;
30
+ /** Install if it is missing; fix permissions if it is merely unreachable.
31
+ * One button, because from the outside they are the same complaint: this
32
+ * machine cannot host desktops. Returns whether a restart is now needed. */
33
+ export declare function installEngine(onLog: (s: string) => void, askPassword?: () => Promise<string | null>, alreadyInstalledButBlocked?: boolean): Promise<{
34
+ needsRestart: boolean;
35
+ }>;
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.REPAIR_NEEDS_RESTART = void 0;
3
4
  exports.installPlan = installPlan;
5
+ exports.repairPlan = repairPlan;
4
6
  exports.runningAsRoot = runningAsRoot;
5
7
  exports.sudoArgv = sudoArgv;
6
8
  exports.installEngine = installEngine;
@@ -37,6 +39,19 @@ function installPlan(platform, isRoot) {
37
39
  manual: 'curl -fsSL https://get.docker.com | sudo sh', needsRoot: true,
38
40
  };
39
41
  }
42
+ /** Docker is there but this user cannot reach its socket. The fix is group
43
+ * membership, not another install — and it needs root, so it takes the same
44
+ * password round trip. */
45
+ function repairPlan(user) {
46
+ const u = user.replace(/[^A-Za-z0-9._-]/g, '');
47
+ return {
48
+ inner: `usermod -aG docker ${u}`,
49
+ manual: `sudo usermod -aG docker ${u}`,
50
+ };
51
+ }
52
+ /** Group membership is fixed for FUTURE processes only, so the daemon that
53
+ * asked for the fix still cannot reach the socket until it restarts. */
54
+ exports.REPAIR_NEEDS_RESTART = true;
40
55
  function runningAsRoot() {
41
56
  return typeof process.getuid === 'function' && process.getuid() === 0;
42
57
  }
@@ -47,11 +62,37 @@ function runningAsRoot() {
47
62
  function sudoArgv(inner) {
48
63
  return { file: 'sudo', args: ['-S', '-p', '', 'sh', '-c', inner] };
49
64
  }
50
- async function installEngine(onLog, askPassword) {
65
+ /** Install if it is missing; fix permissions if it is merely unreachable.
66
+ * One button, because from the outside they are the same complaint: this
67
+ * machine cannot host desktops. Returns whether a restart is now needed. */
68
+ async function installEngine(onLog, askPassword, alreadyInstalledButBlocked) {
51
69
  const plan = installPlan(process.platform, runningAsRoot());
52
70
  let file = plan.file;
53
71
  let args = plan.args;
54
72
  let password = null;
73
+ // Docker present but the socket is refused: repair, do not reinstall.
74
+ const repair = alreadyInstalledButBlocked
75
+ ? repairPlan(process.env.USER || process.env.LOGNAME || 'root')
76
+ : null;
77
+ if (repair) {
78
+ if (runningAsRoot()) {
79
+ onLog(`fixing permissions\n ${repair.inner}\n\n`);
80
+ file = 'sh';
81
+ args = ['-c', repair.inner];
82
+ }
83
+ else if (askPassword) {
84
+ onLog(`This machine needs a sudo password to fix Docker permissions.\n ${repair.manual}\n\n`);
85
+ password = await askPassword();
86
+ if (!password)
87
+ throw new Error(`No password given. Run it on the machine instead:\n ${repair.manual}`);
88
+ const sudo = sudoArgv(repair.inner);
89
+ file = sudo.file;
90
+ args = sudo.args;
91
+ }
92
+ else {
93
+ throw new Error(`Needs root. Run this on the machine:\n ${repair.manual}`);
94
+ }
95
+ }
55
96
  if (!file && plan.needsRoot && askPassword) {
56
97
  onLog('This machine needs a sudo password to install Docker.\n');
57
98
  password = await askPassword();
@@ -86,6 +127,7 @@ async function installEngine(onLog, askPassword) {
86
127
  child.on('error', err => reject(new Error(`${file} is not available here. Install it by hand:\n ${plan.manual}\n(${err.message})`)));
87
128
  child.on('exit', code => code === 0
88
129
  ? resolve()
89
- : reject(new Error(`install failed (exit ${code}). Try by hand:\n ${plan.manual}\n${tail.slice(-400)}`)));
130
+ : reject(new Error(`${repair ? 'permission fix' : 'install'} failed (exit ${code}). Try by hand:\n ${repair ? repair.manual : plan.manual}\n${tail.slice(-400)}`)));
90
131
  });
132
+ return { needsRestart: !!repair && exports.REPAIR_NEEDS_RESTART };
91
133
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@addai/node",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Daemon that pairs a machine with your +Ai account and runs Claude / Codex / Kimi / Gemini agents on its behalf. Reachable via Supabase from Vault, Entity Studio, or any other +Ai surface.",
5
5
  "license": "MIT",
6
6
  "keywords": [