@addai/node 0.30.0 → 0.30.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.
@@ -4,6 +4,28 @@ export declare const DEBUG_PORT = 9222;
4
4
  * it would point the wrapper at itself and exec forever. */
5
5
  export declare const REAL_BROWSERS: string[];
6
6
  export declare const WRAPPER_PATH = "/usr/local/bin/browser";
7
+ /**
8
+ * A line of the wrapper distinctive enough to recognise it by.
9
+ *
10
+ * ⚠️ This exists because the wrapper used to destroy the browser it wraps.
11
+ *
12
+ * In the image WRAPPER_PATH is a SYMLINK — `ln -sf /usr/bin/chromium
13
+ * /usr/local/bin/browser`. `cp` follows a symlink destination and writes
14
+ * THROUGH it, so `cp wrapper /usr/local/bin/browser` overwrote /usr/bin/
15
+ * chromium — the real browser — with a script whose last line is
16
+ *
17
+ * exec /usr/bin/chromium --remote-debugging-port=9222 ... "$@"
18
+ *
19
+ * which is now itself. Each hop re-appended the flags and passed the lot on
20
+ * as "$@", so a single launch exec'd itself until the argument list blew up.
21
+ * From the outside: the browser simply never opened, and the flags appeared
22
+ * hundreds of times over in ps.
23
+ *
24
+ * The write is fixed below. This marker is the second line of defence: a
25
+ * candidate that is really this wrapper is never treated as a real browser,
26
+ * so the loop cannot be rebuilt even if something else recreates the shape.
27
+ */
28
+ export declare const WRAPPER_MARKER = "Written by @addai/node";
7
29
  /** The in-container script that signs the browser in. Beside the extension,
8
30
  * under the same bind mount, so it arrives by the same route. */
9
31
  export declare const SEED_SCRIPT = "/conf/vault/vault-seed.mjs";
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.SEED_SCRIPT = exports.WRAPPER_PATH = exports.REAL_BROWSERS = exports.DEBUG_PORT = void 0;
36
+ exports.SEED_SCRIPT = exports.WRAPPER_MARKER = exports.WRAPPER_PATH = exports.REAL_BROWSERS = exports.DEBUG_PORT = void 0;
37
37
  exports.wrapperScript = wrapperScript;
38
38
  exports.wrapperIsCurrent = wrapperIsCurrent;
39
39
  exports.ensureBrowserWrapper = ensureBrowserWrapper;
@@ -65,6 +65,28 @@ exports.REAL_BROWSERS = [
65
65
  '/usr/bin/chromium-browser',
66
66
  ];
67
67
  exports.WRAPPER_PATH = '/usr/local/bin/browser';
68
+ /**
69
+ * A line of the wrapper distinctive enough to recognise it by.
70
+ *
71
+ * ⚠️ This exists because the wrapper used to destroy the browser it wraps.
72
+ *
73
+ * In the image WRAPPER_PATH is a SYMLINK — `ln -sf /usr/bin/chromium
74
+ * /usr/local/bin/browser`. `cp` follows a symlink destination and writes
75
+ * THROUGH it, so `cp wrapper /usr/local/bin/browser` overwrote /usr/bin/
76
+ * chromium — the real browser — with a script whose last line is
77
+ *
78
+ * exec /usr/bin/chromium --remote-debugging-port=9222 ... "$@"
79
+ *
80
+ * which is now itself. Each hop re-appended the flags and passed the lot on
81
+ * as "$@", so a single launch exec'd itself until the argument list blew up.
82
+ * From the outside: the browser simply never opened, and the flags appeared
83
+ * hundreds of times over in ps.
84
+ *
85
+ * The write is fixed below. This marker is the second line of defence: a
86
+ * candidate that is really this wrapper is never treated as a real browser,
87
+ * so the loop cannot be rebuilt even if something else recreates the shape.
88
+ */
89
+ exports.WRAPPER_MARKER = 'Written by @addai/node';
68
90
  /** The in-container script that signs the browser in. Beside the extension,
69
91
  * under the same bind mount, so it arrives by the same route. */
70
92
  exports.SEED_SCRIPT = '/conf/vault/vault-seed.mjs';
@@ -112,10 +134,28 @@ function wrapperIsCurrent(content, realBrowser, extensionDir) {
112
134
  * cheap execs and no change.
113
135
  */
114
136
  async function ensureBrowserWrapper(run, confDir, extensionDir) {
137
+ // Each candidate is classified, not just found: one that is really this
138
+ // wrapper (see WRAPPER_MARKER) is a browser an older build overwrote, and
139
+ // pointing a new wrapper at it would rebuild the exec loop.
115
140
  const found = await run(['sh', '-c',
116
- exports.REAL_BROWSERS.map(b => `[ -x ${b} ] && echo ${b}`).join(' || ') + ' || true']);
117
- const real = found.out.trim().split('\n')[0]?.trim();
141
+ `for b in ${exports.REAL_BROWSERS.join(' ')}; do [ -x "$b" ] || continue; ` +
142
+ `if head -c 512 "$b" 2>/dev/null | grep -qF '${exports.WRAPPER_MARKER}'; ` +
143
+ `then echo "wrapped $b"; else echo "real $b"; fi; done`]);
144
+ const lines = found.out.trim().split('\n').map(l => l.trim()).filter(Boolean);
145
+ const real = lines.find(l => l.startsWith('real '))?.slice(5);
118
146
  if (!real) {
147
+ const wrapped = lines.find(l => l.startsWith('wrapped '))?.slice(8);
148
+ if (wrapped) {
149
+ // Deliberately not repaired from here. Putting the browser back means
150
+ // reinstalling its package — a network install, on every start, on a
151
+ // machine that is somebody's desktop — and this path is meant to be two
152
+ // cheap execs. Rebuilding the desktop restores it from the image.
153
+ return {
154
+ changed: false, browser: null,
155
+ note: `${wrapped} was overwritten by an older version of this wrapper `
156
+ + 'and no longer starts a browser. Rebuild this desktop to restore it.',
157
+ };
158
+ }
119
159
  return { changed: false, browser: null, note: 'this desktop has no browser installed' };
120
160
  }
121
161
  const current = await run(['sh', '-c', `cat ${exports.WRAPPER_PATH} 2>/dev/null || true`]);
@@ -127,8 +167,15 @@ async function ensureBrowserWrapper(run, confDir, extensionDir) {
127
167
  // sudo, not `docker exec -u root`: the image gives the entity passwordless
128
168
  // sudo inside its own container, and going through the same exec path as
129
169
  // everything else keeps one way of talking to a desktop rather than two.
170
+ // ⚠️ rm FIRST, and it is the whole fix. In the image WRAPPER_PATH is a
171
+ // symlink to the real browser, and `cp` follows a symlink destination —
172
+ // writing through it, over the browser, rather than replacing the link.
173
+ // Unlinking first makes the wrapper a file of its own and leaves whatever
174
+ // it pointed at alone. (`cp --remove-destination` does the same in one
175
+ // call; this shape says why out loud and needs no GNU-only flag.)
130
176
  const put = await run(['sh', '-c',
131
- `sudo cp /conf/learn/browser ${exports.WRAPPER_PATH} && sudo chmod +x ${exports.WRAPPER_PATH}`]);
177
+ `sudo rm -f ${exports.WRAPPER_PATH} && sudo cp /conf/learn/browser ${exports.WRAPPER_PATH} `
178
+ + `&& sudo chmod +x ${exports.WRAPPER_PATH}`]);
132
179
  if (!put.ok) {
133
180
  return { changed: false, browser: real, note: `could not install the browser wrapper: ${put.out.trim().slice(0, 200)}` };
134
181
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@addai/node",
3
- "version": "0.30.0",
3
+ "version": "0.30.1",
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": [