@bridge4dev/runner 0.22.1 → 0.27.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.
- package/dist/agent-auth.d.ts +29 -0
- package/dist/agent-auth.js +136 -0
- package/dist/auth-relay.d.ts +62 -4
- package/dist/auth-relay.js +423 -25
- package/dist/environment.d.ts +186 -0
- package/dist/environment.js +433 -0
- package/dist/git.d.ts +10 -0
- package/dist/git.js +94 -5
- package/dist/index.js +466 -21
- package/dist/paths.d.ts +9 -0
- package/dist/paths.js +11 -0
- package/dist/protocol.d.ts +7 -7
- package/dist/recipe-schema.d.ts +1 -1
- package/dist/self-update.d.ts +55 -0
- package/dist/self-update.js +164 -25
- package/dist/service-unit.d.ts +13 -1
- package/dist/service-unit.js +41 -10
- package/dist/supervisor.js +35 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/protocol.d.ts
CHANGED
|
@@ -179,11 +179,11 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
|
|
|
179
179
|
id: string;
|
|
180
180
|
title: string;
|
|
181
181
|
}[];
|
|
182
|
-
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
183
182
|
mcp?: {
|
|
184
183
|
url: string;
|
|
185
184
|
token: string;
|
|
186
185
|
} | undefined;
|
|
186
|
+
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
187
187
|
model?: string | null | undefined;
|
|
188
188
|
effort?: string | null | undefined;
|
|
189
189
|
epoch?: number | undefined;
|
|
@@ -381,11 +381,11 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
381
381
|
id: string;
|
|
382
382
|
title: string;
|
|
383
383
|
}[];
|
|
384
|
-
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
385
384
|
mcp?: {
|
|
386
385
|
url: string;
|
|
387
386
|
token: string;
|
|
388
387
|
} | undefined;
|
|
388
|
+
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
389
389
|
model?: string | null | undefined;
|
|
390
390
|
effort?: string | null | undefined;
|
|
391
391
|
epoch?: number | undefined;
|
|
@@ -469,11 +469,11 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
469
469
|
id: string;
|
|
470
470
|
title: string;
|
|
471
471
|
}[];
|
|
472
|
-
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
473
472
|
mcp?: {
|
|
474
473
|
url: string;
|
|
475
474
|
token: string;
|
|
476
475
|
} | undefined;
|
|
476
|
+
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
477
477
|
model?: string | null | undefined;
|
|
478
478
|
effort?: string | null | undefined;
|
|
479
479
|
epoch?: number | undefined;
|
|
@@ -695,11 +695,11 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
695
695
|
id: string;
|
|
696
696
|
title: string;
|
|
697
697
|
}[];
|
|
698
|
-
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
699
698
|
mcp?: {
|
|
700
699
|
url: string;
|
|
701
700
|
token: string;
|
|
702
701
|
} | undefined;
|
|
702
|
+
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
703
703
|
model?: string | null | undefined;
|
|
704
704
|
effort?: string | null | undefined;
|
|
705
705
|
epoch?: number | undefined;
|
|
@@ -778,11 +778,11 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
778
778
|
id: string;
|
|
779
779
|
title: string;
|
|
780
780
|
}[];
|
|
781
|
-
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
782
781
|
mcp?: {
|
|
783
782
|
url: string;
|
|
784
783
|
token: string;
|
|
785
784
|
} | undefined;
|
|
785
|
+
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
786
786
|
model?: string | null | undefined;
|
|
787
787
|
effort?: string | null | undefined;
|
|
788
788
|
epoch?: number | undefined;
|
|
@@ -961,16 +961,16 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
961
961
|
sessionId: z.ZodOptional<z.ZodString>;
|
|
962
962
|
args: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
963
963
|
}, "strip", z.ZodTypeAny, {
|
|
964
|
+
name: string;
|
|
964
965
|
type: "command";
|
|
965
966
|
requestId: string;
|
|
966
|
-
name: string;
|
|
967
967
|
sessionId?: string | undefined;
|
|
968
968
|
workspaceId?: string | undefined;
|
|
969
969
|
args?: Record<string, unknown> | undefined;
|
|
970
970
|
}, {
|
|
971
|
+
name: string;
|
|
971
972
|
type: "command";
|
|
972
973
|
requestId: string;
|
|
973
|
-
name: string;
|
|
974
974
|
sessionId?: string | undefined;
|
|
975
975
|
workspaceId?: string | undefined;
|
|
976
976
|
args?: Record<string, unknown> | undefined;
|
package/dist/recipe-schema.d.ts
CHANGED
|
@@ -253,6 +253,7 @@ export declare const ProjectRecipeSchema: z.ZodObject<{
|
|
|
253
253
|
shaPath?: string | undefined;
|
|
254
254
|
} | undefined;
|
|
255
255
|
}, {
|
|
256
|
+
version?: 1 | undefined;
|
|
256
257
|
preview?: {
|
|
257
258
|
run: string;
|
|
258
259
|
url?: string | undefined;
|
|
@@ -260,7 +261,6 @@ export declare const ProjectRecipeSchema: z.ZodObject<{
|
|
|
260
261
|
timeoutSec?: number | undefined;
|
|
261
262
|
stop?: string | undefined;
|
|
262
263
|
} | undefined;
|
|
263
|
-
version?: 1 | undefined;
|
|
264
264
|
notes?: string | undefined;
|
|
265
265
|
steps?: {
|
|
266
266
|
verify?: {
|
package/dist/self-update.d.ts
CHANGED
|
@@ -62,6 +62,45 @@ export interface SelfUpdateOptions {
|
|
|
62
62
|
* has one above `packages/runner`.
|
|
63
63
|
*/
|
|
64
64
|
export declare function resolveInstalledPackageDir(entry?: string): string | null;
|
|
65
|
+
/**
|
|
66
|
+
* The npm prefix the RUNNING package was installed into.
|
|
67
|
+
*
|
|
68
|
+
* This is the whole fix for the dedicated-user layout. `npm install -g` obeys
|
|
69
|
+
* npm's *configured* prefix, and `--prefix` is a per-invocation flag that npm
|
|
70
|
+
* persists nowhere — so the install the instructions prescribed for a dedicated
|
|
71
|
+
* user (`npm install -g --prefix ~/.local …`) produced a runner in
|
|
72
|
+
* `~/.local/lib/node_modules` while every later `npm install -g` aimed at
|
|
73
|
+
* `/usr`. Measured on a live server: `npm prefix -g` → `/usr`,
|
|
74
|
+
* `installIsWritable` → true (both checked directories really are the user's),
|
|
75
|
+
* and then npm dies with EACCES *inside* the update — the exact
|
|
76
|
+
* «the permissions to access this file as the current user» the owner pasted.
|
|
77
|
+
*
|
|
78
|
+
* Deriving the prefix from where we physically are is the truthful answer: it
|
|
79
|
+
* cannot disagree with reality, and it costs no subprocess. Returns null for a
|
|
80
|
+
* layout that is not a global npm install (a source checkout, a vendored copy),
|
|
81
|
+
* where the caller should fall back to `npm prefix -g`.
|
|
82
|
+
*
|
|
83
|
+
* `<prefix>/lib/node_modules/@scope/pkg` → `<prefix>`
|
|
84
|
+
* `<prefix>/lib/node_modules/pkg` → `<prefix>`
|
|
85
|
+
*/
|
|
86
|
+
export declare function installPrefixFor(packageDir?: string | null): string | null;
|
|
87
|
+
/**
|
|
88
|
+
* Can this user actually replace the installed package?
|
|
89
|
+
*
|
|
90
|
+
* The two legitimate installs part ways here. `npm install -g` run by root puts
|
|
91
|
+
* the package in `/usr/lib/node_modules` owned by root; the daemon then runs as
|
|
92
|
+
* a dedicated user, who cannot write there. The button was offered anyway and
|
|
93
|
+
* failed halfway through npm with «the permissions to access this file as the
|
|
94
|
+
* current user» — a dashboard button that cannot work, and an error in npm's
|
|
95
|
+
* words rather than ours.
|
|
96
|
+
*
|
|
97
|
+
* Three directories matter, because npm rewrites all three: the package itself,
|
|
98
|
+
* the `node_modules` above it, and `<prefix>/bin`, where the command symlink
|
|
99
|
+
* lives. `bin` was missing here and is not hypothetical — a prefix whose
|
|
100
|
+
* `lib/node_modules` was handed over with chown while `bin` stayed root-owned
|
|
101
|
+
* passes the first two and still fails.
|
|
102
|
+
*/
|
|
103
|
+
export declare function installIsWritable(packageDir?: string | null): boolean;
|
|
65
104
|
/**
|
|
66
105
|
* Is something going to restart us?
|
|
67
106
|
*
|
|
@@ -78,5 +117,21 @@ export declare function isSupervisedProcess(env?: NodeJS.ProcessEnv): boolean;
|
|
|
78
117
|
* never be talked into fetching its own replacement over http.
|
|
79
118
|
*/
|
|
80
119
|
export declare function isTrustedTarballUrl(tarballUrl: string, apiUrl: string): boolean;
|
|
120
|
+
/**
|
|
121
|
+
* The command a person can paste on the server to update this exact machine.
|
|
122
|
+
*
|
|
123
|
+
* Built by the runner rather than by the dashboard because only the runner
|
|
124
|
+
* knows the three things that make it correct: which prefix it lives in, which
|
|
125
|
+
* user it runs as, and that user's uid for `XDG_RUNTIME_DIR`. The dashboard
|
|
126
|
+
* used to assemble a generic `npm install -g <url> && systemctl --user restart`
|
|
127
|
+
* and then patch it with string surgery — which produced a command that could
|
|
128
|
+
* not work on precisely the machines that needed it.
|
|
129
|
+
*/
|
|
130
|
+
export declare function manualUpdateCommand(tarballUrl: string, options?: {
|
|
131
|
+
prefix?: string | null;
|
|
132
|
+
user?: string;
|
|
133
|
+
uid?: number;
|
|
134
|
+
packageDir?: string | null;
|
|
135
|
+
}): string;
|
|
81
136
|
export declare function selfUpdate(options: SelfUpdateOptions): Promise<SelfUpdateOutcome>;
|
|
82
137
|
//# sourceMappingURL=self-update.d.ts.map
|
package/dist/self-update.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { promisify } from 'node:util';
|
|
5
6
|
import { log } from './log.js';
|
|
@@ -54,6 +55,85 @@ export function resolveInstalledPackageDir(entry = process.argv[1] ?? '') {
|
|
|
54
55
|
}
|
|
55
56
|
return null;
|
|
56
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* The npm prefix the RUNNING package was installed into.
|
|
60
|
+
*
|
|
61
|
+
* This is the whole fix for the dedicated-user layout. `npm install -g` obeys
|
|
62
|
+
* npm's *configured* prefix, and `--prefix` is a per-invocation flag that npm
|
|
63
|
+
* persists nowhere — so the install the instructions prescribed for a dedicated
|
|
64
|
+
* user (`npm install -g --prefix ~/.local …`) produced a runner in
|
|
65
|
+
* `~/.local/lib/node_modules` while every later `npm install -g` aimed at
|
|
66
|
+
* `/usr`. Measured on a live server: `npm prefix -g` → `/usr`,
|
|
67
|
+
* `installIsWritable` → true (both checked directories really are the user's),
|
|
68
|
+
* and then npm dies with EACCES *inside* the update — the exact
|
|
69
|
+
* «the permissions to access this file as the current user» the owner pasted.
|
|
70
|
+
*
|
|
71
|
+
* Deriving the prefix from where we physically are is the truthful answer: it
|
|
72
|
+
* cannot disagree with reality, and it costs no subprocess. Returns null for a
|
|
73
|
+
* layout that is not a global npm install (a source checkout, a vendored copy),
|
|
74
|
+
* where the caller should fall back to `npm prefix -g`.
|
|
75
|
+
*
|
|
76
|
+
* `<prefix>/lib/node_modules/@scope/pkg` → `<prefix>`
|
|
77
|
+
* `<prefix>/lib/node_modules/pkg` → `<prefix>`
|
|
78
|
+
*/
|
|
79
|
+
export function installPrefixFor(packageDir = resolveInstalledPackageDir()) {
|
|
80
|
+
if (!packageDir)
|
|
81
|
+
return null;
|
|
82
|
+
const marker = `${path.sep}node_modules${path.sep}`;
|
|
83
|
+
const at = packageDir.lastIndexOf(marker);
|
|
84
|
+
if (at < 0)
|
|
85
|
+
return null;
|
|
86
|
+
const nodeModules = packageDir.slice(0, at + marker.length - 1);
|
|
87
|
+
const lib = path.dirname(nodeModules);
|
|
88
|
+
// Anything else is not the layout `npm install -g --prefix` produces, and
|
|
89
|
+
// guessing a prefix that npm would not use is worse than not guessing.
|
|
90
|
+
if (path.basename(lib) !== 'lib')
|
|
91
|
+
return null;
|
|
92
|
+
return path.dirname(lib);
|
|
93
|
+
}
|
|
94
|
+
function isWritable(target) {
|
|
95
|
+
try {
|
|
96
|
+
fs.accessSync(target, fs.constants.W_OK);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Can this user actually replace the installed package?
|
|
105
|
+
*
|
|
106
|
+
* The two legitimate installs part ways here. `npm install -g` run by root puts
|
|
107
|
+
* the package in `/usr/lib/node_modules` owned by root; the daemon then runs as
|
|
108
|
+
* a dedicated user, who cannot write there. The button was offered anyway and
|
|
109
|
+
* failed halfway through npm with «the permissions to access this file as the
|
|
110
|
+
* current user» — a dashboard button that cannot work, and an error in npm's
|
|
111
|
+
* words rather than ours.
|
|
112
|
+
*
|
|
113
|
+
* Three directories matter, because npm rewrites all three: the package itself,
|
|
114
|
+
* the `node_modules` above it, and `<prefix>/bin`, where the command symlink
|
|
115
|
+
* lives. `bin` was missing here and is not hypothetical — a prefix whose
|
|
116
|
+
* `lib/node_modules` was handed over with chown while `bin` stayed root-owned
|
|
117
|
+
* passes the first two and still fails.
|
|
118
|
+
*/
|
|
119
|
+
export function installIsWritable(packageDir = resolveInstalledPackageDir()) {
|
|
120
|
+
if (!packageDir)
|
|
121
|
+
return false;
|
|
122
|
+
const marker = `${path.sep}node_modules${path.sep}`;
|
|
123
|
+
const at = packageDir.lastIndexOf(marker);
|
|
124
|
+
// `<prefix>/lib/node_modules/@scope/pkg` → `<prefix>/lib/node_modules`
|
|
125
|
+
const nodeModules = at >= 0 ? packageDir.slice(0, at + marker.length - 1) : path.dirname(packageDir);
|
|
126
|
+
if (!isWritable(packageDir) || !isWritable(nodeModules))
|
|
127
|
+
return false;
|
|
128
|
+
const prefix = installPrefixFor(packageDir);
|
|
129
|
+
// No `bin` yet is fine — npm creates it. An existing one we cannot write is not.
|
|
130
|
+
if (prefix) {
|
|
131
|
+
const bin = path.join(prefix, 'bin');
|
|
132
|
+
if (fs.existsSync(bin) && !isWritable(bin))
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
57
137
|
/**
|
|
58
138
|
* Is something going to restart us?
|
|
59
139
|
*
|
|
@@ -110,17 +190,23 @@ const COMMAND_NAME = 'devbridge-runner';
|
|
|
110
190
|
* command — the stable thing, referenced by a unit file we do not control —
|
|
111
191
|
* points at the new one. So we follow the command.
|
|
112
192
|
*/
|
|
113
|
-
async function commandOwner(exec) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
193
|
+
async function commandOwner(exec, installPrefix = null) {
|
|
194
|
+
// Where we actually live outranks what npm is configured to think. Under the
|
|
195
|
+
// dedicated-user layout `npm prefix -g` answers `/usr` — a directory that on
|
|
196
|
+
// such a host holds no runner at all — so asking npm first returned null and
|
|
197
|
+
// took the EEXIST rename-recovery and the unit repair down with it.
|
|
198
|
+
let prefix = installPrefix;
|
|
199
|
+
if (!prefix) {
|
|
200
|
+
try {
|
|
201
|
+
const result = await exec('npm', ['prefix', '-g'], {
|
|
202
|
+
timeout: VERIFY_TIMEOUT_MS,
|
|
203
|
+
env: npmEnv(),
|
|
204
|
+
});
|
|
205
|
+
prefix = result.stdout.trim();
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
124
210
|
}
|
|
125
211
|
if (!prefix)
|
|
126
212
|
return null;
|
|
@@ -137,13 +223,55 @@ async function commandOwner(exec) {
|
|
|
137
223
|
return null;
|
|
138
224
|
}
|
|
139
225
|
}
|
|
140
|
-
function installArgs(source) {
|
|
226
|
+
function installArgs(source, prefix) {
|
|
141
227
|
// `--ignore-scripts` matches the documented install: this package and its whole
|
|
142
228
|
// tree have no install/postinstall scripts, so nothing legitimate is skipped —
|
|
143
229
|
// and an update pulled over the network gets no chance to run anything at
|
|
144
230
|
// install time. `--loglevel=error` because npm's ERESOLVE warning about zod is
|
|
145
231
|
// expected, harmless and long enough to bury the line that matters.
|
|
146
|
-
|
|
232
|
+
//
|
|
233
|
+
// `--prefix` names the prefix the runner is ALREADY installed in. Without it
|
|
234
|
+
// npm uses its configured global prefix, which on a dedicated-user install is
|
|
235
|
+
// a directory the daemon cannot write — and, on the rarer host where it can,
|
|
236
|
+
// npm cheerfully installs a SECOND copy somewhere the service does not exec,
|
|
237
|
+
// reports success, and the runner restarts on the old version forever.
|
|
238
|
+
return [
|
|
239
|
+
'install',
|
|
240
|
+
'-g',
|
|
241
|
+
'--ignore-scripts',
|
|
242
|
+
'--loglevel=error',
|
|
243
|
+
...(prefix ? ['--prefix', prefix] : []),
|
|
244
|
+
source,
|
|
245
|
+
];
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* The command a person can paste on the server to update this exact machine.
|
|
249
|
+
*
|
|
250
|
+
* Built by the runner rather than by the dashboard because only the runner
|
|
251
|
+
* knows the three things that make it correct: which prefix it lives in, which
|
|
252
|
+
* user it runs as, and that user's uid for `XDG_RUNTIME_DIR`. The dashboard
|
|
253
|
+
* used to assemble a generic `npm install -g <url> && systemctl --user restart`
|
|
254
|
+
* and then patch it with string surgery — which produced a command that could
|
|
255
|
+
* not work on precisely the machines that needed it.
|
|
256
|
+
*/
|
|
257
|
+
export function manualUpdateCommand(tarballUrl, options = {}) {
|
|
258
|
+
const packageDir = options.packageDir === undefined ? resolveInstalledPackageDir() : options.packageDir;
|
|
259
|
+
const prefix = options.prefix === undefined ? installPrefixFor(packageDir) : options.prefix;
|
|
260
|
+
const user = options.user ?? os.userInfo().username;
|
|
261
|
+
const uid = options.uid ?? (typeof process.getuid === 'function' ? process.getuid() : -1);
|
|
262
|
+
const install = ['npm install -g --ignore-scripts --loglevel=error']
|
|
263
|
+
.concat(prefix ? [`--prefix ${prefix}`] : [])
|
|
264
|
+
.concat([tarballUrl])
|
|
265
|
+
.join(' ');
|
|
266
|
+
const restart = 'systemctl --user restart devbridge-runner';
|
|
267
|
+
// Writable by us → one line, run as ourselves. Not writable → the install
|
|
268
|
+
// half needs root and the restart half needs the daemon's own user, and
|
|
269
|
+
// «run it as root» alone is how people ended up with a new package and an
|
|
270
|
+
// old process still running.
|
|
271
|
+
if (installIsWritable(packageDir))
|
|
272
|
+
return `${install} && ${restart}`;
|
|
273
|
+
const runtime = uid >= 0 ? `/run/user/${uid}` : `/run/user/$(id -u ${user})`;
|
|
274
|
+
return `sudo ${install} && sudo -iu ${user} env XDG_RUNTIME_DIR=${runtime} ${restart}`;
|
|
147
275
|
}
|
|
148
276
|
/**
|
|
149
277
|
* Install a global package, clearing the way if a DIFFERENTLY-NAMED build of this
|
|
@@ -156,25 +284,22 @@ function installArgs(source) {
|
|
|
156
284
|
* one owner instead of two, which is also the only state the NEXT update can
|
|
157
285
|
* work from.
|
|
158
286
|
*/
|
|
159
|
-
async function installGlobal(exec, source) {
|
|
287
|
+
async function installGlobal(exec, source, prefix) {
|
|
160
288
|
try {
|
|
161
|
-
await exec('npm', installArgs(source), { timeout: NPM_TIMEOUT_MS, env: npmEnv() });
|
|
289
|
+
await exec('npm', installArgs(source, prefix), { timeout: NPM_TIMEOUT_MS, env: npmEnv() });
|
|
162
290
|
return;
|
|
163
291
|
}
|
|
164
292
|
catch (error) {
|
|
165
293
|
if (!/EEXIST/i.test(describe(error)))
|
|
166
294
|
throw error;
|
|
167
|
-
const owner = await commandOwner(exec);
|
|
295
|
+
const owner = await commandOwner(exec, prefix);
|
|
168
296
|
if (!owner)
|
|
169
297
|
throw error;
|
|
170
298
|
log.warn('self-update: the command belongs to another package — retiring it', {
|
|
171
299
|
package: owner.name,
|
|
172
300
|
});
|
|
173
|
-
await exec('npm', ['uninstall', '-g', '--loglevel=error', owner.name], {
|
|
174
|
-
|
|
175
|
-
env: npmEnv(),
|
|
176
|
-
});
|
|
177
|
-
await exec('npm', installArgs(source), { timeout: NPM_TIMEOUT_MS, env: npmEnv() });
|
|
301
|
+
await exec('npm', ['uninstall', '-g', '--loglevel=error', ...(prefix ? ['--prefix', prefix] : []), owner.name], { timeout: NPM_TIMEOUT_MS, env: npmEnv() });
|
|
302
|
+
await exec('npm', installArgs(source, prefix), { timeout: NPM_TIMEOUT_MS, env: npmEnv() });
|
|
178
303
|
}
|
|
179
304
|
}
|
|
180
305
|
/**
|
|
@@ -218,6 +343,20 @@ export async function selfUpdate(options) {
|
|
|
218
343
|
if (!packageDir) {
|
|
219
344
|
return fail('This runner runs from a source checkout, not from an installed package — update it with git instead.');
|
|
220
345
|
}
|
|
346
|
+
// Installed by one user, run by another — the usual shape being `npm install
|
|
347
|
+
// -g` as root with the daemon under a dedicated user. npm gets far enough to
|
|
348
|
+
// start rewriting the package and then stops with EACCES, so the check has to
|
|
349
|
+
// happen BEFORE anything is touched. Refused with the two commands that work,
|
|
350
|
+
// because «run it as root» alone leaves out the restart, which needs the
|
|
351
|
+
// runner's own user.
|
|
352
|
+
if (!installIsWritable(packageDir)) {
|
|
353
|
+
const user = os.userInfo().username;
|
|
354
|
+
return fail(`The runner package in ${packageDir} belongs to another user, and this daemon runs as ${user}, ` +
|
|
355
|
+
'so it cannot replace itself. Run this on the server instead:\n ' +
|
|
356
|
+
manualUpdateCommand(options.tarballUrl, { packageDir }));
|
|
357
|
+
}
|
|
358
|
+
// Derived once and threaded through every npm call below.
|
|
359
|
+
const prefix = installPrefixFor(packageDir);
|
|
221
360
|
// Pack the current version FIRST: without a rollback artefact there is no
|
|
222
361
|
// honest way back if the new build turns out to be broken.
|
|
223
362
|
const rollbackDir = path.join(stateDir(), 'rollback');
|
|
@@ -242,7 +381,7 @@ export async function selfUpdate(options) {
|
|
|
242
381
|
return fail('Could not prepare a rollback copy of the current version — update aborted');
|
|
243
382
|
}
|
|
244
383
|
try {
|
|
245
|
-
await installGlobal(exec, options.tarballUrl);
|
|
384
|
+
await installGlobal(exec, options.tarballUrl, prefix);
|
|
246
385
|
}
|
|
247
386
|
catch (error) {
|
|
248
387
|
return fail(`Install failed: ${describe(error)}`, { rollbackTarball });
|
|
@@ -250,7 +389,7 @@ export async function selfUpdate(options) {
|
|
|
250
389
|
// Where the new build actually landed. After a rename `packageDir` is the
|
|
251
390
|
// directory we just retired, so its manifest would report the OLD version —
|
|
252
391
|
// and the smoke test below would run code that no longer exists.
|
|
253
|
-
const installed = await commandOwner(exec);
|
|
392
|
+
const installed = await commandOwner(exec, prefix);
|
|
254
393
|
const newPackageDir = installed?.dir ?? packageDir;
|
|
255
394
|
const toVersion = readVersion(newPackageDir) ?? undefined;
|
|
256
395
|
// The real test: does the newly installed build start? `--version` loads the
|
|
@@ -273,12 +412,12 @@ export async function selfUpdate(options) {
|
|
|
273
412
|
// Through the same door as the install above: if the failed update renamed
|
|
274
413
|
// the package, the command now belongs to the new name and putting the old
|
|
275
414
|
// one back hits the very same EEXIST.
|
|
276
|
-
await installGlobal(exec, rollbackTarball);
|
|
415
|
+
await installGlobal(exec, rollbackTarball, prefix);
|
|
277
416
|
return fail(`The new version did not start (${detail}). The previous version was restored and the runner keeps working.`, { rollbackTarball, ...(toVersion ? { toVersion } : {}) });
|
|
278
417
|
}
|
|
279
418
|
catch (rollbackError) {
|
|
280
419
|
return fail(`The new version did not start (${detail}) and the rollback failed too (${describe(rollbackError)}). ` +
|
|
281
|
-
`Restore it on the server with: npm install -g ${rollbackTarball}`, { rollbackTarball, ...(toVersion ? { toVersion } : {}) });
|
|
420
|
+
`Restore it on the server with: npm install -g${prefix ? ` --prefix ${prefix}` : ''} ${rollbackTarball}`, { rollbackTarball, ...(toVersion ? { toVersion } : {}) });
|
|
282
421
|
}
|
|
283
422
|
}
|
|
284
423
|
// The service unit may be pinned to a file inside the directory this update
|
package/dist/service-unit.d.ts
CHANGED
|
@@ -15,12 +15,24 @@
|
|
|
15
15
|
*/
|
|
16
16
|
export declare const SERVICE_NAME = "devbridge-runner";
|
|
17
17
|
/** `<prefix>/bin/devbridge-runner` for an installed package, else the script. */
|
|
18
|
+
/**
|
|
19
|
+
* A path that will not exist after a reboot.
|
|
20
|
+
*
|
|
21
|
+
* `fnm` (and `nvm`/`volta` in the same spirit) puts the active version's `bin`
|
|
22
|
+
* into a per-shell directory under `/run/user/<uid>/fnm_multishells/…` — tmpfs,
|
|
23
|
+
* created for one shell session. `command -v devbridge-runner` resolves there,
|
|
24
|
+
* so baking it into a unit produces a service that works until the shell that
|
|
25
|
+
* installed it goes away, and then fails with status=127 forever. Observed on a
|
|
26
|
+
* real install, 2026-07-31, on a ROOT install — this is not a dedicated-user
|
|
27
|
+
* problem, it is a per-user node manager problem.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isEphemeralPath(target: string): boolean;
|
|
18
30
|
export declare function unitExecTarget(argv1?: string): {
|
|
19
31
|
execStart: string;
|
|
20
32
|
viaCommand: boolean;
|
|
21
33
|
};
|
|
22
34
|
export declare function unitPath(home?: string): string;
|
|
23
|
-
export declare function buildUnit(execStart?: string): string;
|
|
35
|
+
export declare function buildUnit(execStart?: string, nodeBinary?: string): string;
|
|
24
36
|
/**
|
|
25
37
|
* Resource policy for the service, and why it does not live in the unit above.
|
|
26
38
|
*
|
package/dist/service-unit.js
CHANGED
|
@@ -20,10 +20,31 @@ import { systemdUserHome } from './paths.js';
|
|
|
20
20
|
export const SERVICE_NAME = 'devbridge-runner';
|
|
21
21
|
const COMMAND_NAME = 'devbridge-runner';
|
|
22
22
|
/** `<prefix>/bin/devbridge-runner` for an installed package, else the script. */
|
|
23
|
+
/**
|
|
24
|
+
* A path that will not exist after a reboot.
|
|
25
|
+
*
|
|
26
|
+
* `fnm` (and `nvm`/`volta` in the same spirit) puts the active version's `bin`
|
|
27
|
+
* into a per-shell directory under `/run/user/<uid>/fnm_multishells/…` — tmpfs,
|
|
28
|
+
* created for one shell session. `command -v devbridge-runner` resolves there,
|
|
29
|
+
* so baking it into a unit produces a service that works until the shell that
|
|
30
|
+
* installed it goes away, and then fails with status=127 forever. Observed on a
|
|
31
|
+
* real install, 2026-07-31, on a ROOT install — this is not a dedicated-user
|
|
32
|
+
* problem, it is a per-user node manager problem.
|
|
33
|
+
*/
|
|
34
|
+
export function isEphemeralPath(target) {
|
|
35
|
+
// Deliberately narrow: `/run` and `/dev/shm` are tmpfs by definition, and
|
|
36
|
+
// `fnm_multishells` is named because fnm also offers non-tmpfs layouts. `/tmp`
|
|
37
|
+
// is NOT here — a package installed there is odd but survives, and tests build
|
|
38
|
+
// their fake installs in temp directories.
|
|
39
|
+
return (/^\/(run|proc)\//.test(target) ||
|
|
40
|
+
target.startsWith('/dev/shm/') ||
|
|
41
|
+
target.includes('fnm_multishells'));
|
|
42
|
+
}
|
|
23
43
|
export function unitExecTarget(argv1 = process.argv[1] ?? '') {
|
|
24
|
-
// Invoked through the command itself: that is already the stable path
|
|
44
|
+
// Invoked through the command itself: that is already the stable path —
|
|
45
|
+
// unless it lives in a directory that disappears with the shell.
|
|
25
46
|
try {
|
|
26
|
-
if (fs.lstatSync(argv1).isSymbolicLink()) {
|
|
47
|
+
if (fs.lstatSync(argv1).isSymbolicLink() && !isEphemeralPath(path.resolve(argv1))) {
|
|
27
48
|
return { execStart: path.resolve(argv1), viaCommand: true };
|
|
28
49
|
}
|
|
29
50
|
}
|
|
@@ -43,8 +64,9 @@ export function unitExecTarget(argv1 = process.argv[1] ?? '') {
|
|
|
43
64
|
let dir = path.dirname(script);
|
|
44
65
|
for (let i = 0; i < 6; i++) {
|
|
45
66
|
const candidate = path.join(dir, 'bin', COMMAND_NAME);
|
|
46
|
-
if (fs.existsSync(candidate))
|
|
67
|
+
if (fs.existsSync(candidate) && !isEphemeralPath(candidate)) {
|
|
47
68
|
return { execStart: candidate, viaCommand: true };
|
|
69
|
+
}
|
|
48
70
|
const parent = path.dirname(dir);
|
|
49
71
|
if (parent === dir)
|
|
50
72
|
break;
|
|
@@ -56,13 +78,19 @@ export function unitExecTarget(argv1 = process.argv[1] ?? '') {
|
|
|
56
78
|
export function unitPath(home = systemdUserHome()) {
|
|
57
79
|
return path.join(home, '.config', 'systemd', 'user', `${SERVICE_NAME}.service`);
|
|
58
80
|
}
|
|
59
|
-
export function buildUnit(execStart) {
|
|
81
|
+
export function buildUnit(execStart, nodeBinary = process.execPath) {
|
|
60
82
|
const target = execStart ?? unitExecTarget().execStart;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
83
|
+
/**
|
|
84
|
+
* The interpreter is ALWAYS spelled out, even for the command symlink.
|
|
85
|
+
*
|
|
86
|
+
* The file carries `#!/usr/bin/env node`, and relying on that shebang means
|
|
87
|
+
* relying on `node` being on the PATH systemd gives the service — which it is
|
|
88
|
+
* not when node came from fnm/nvm/volta. That produced `status=127` and an
|
|
89
|
+
* endless restart loop on a real machine. Node runs a file with a shebang
|
|
90
|
+
* perfectly well (it is a comment to it), so naming the interpreter costs
|
|
91
|
+
* nothing and removes the dependency entirely.
|
|
92
|
+
*/
|
|
93
|
+
const command = `${nodeBinary} ${target} daemon`;
|
|
66
94
|
return ([
|
|
67
95
|
'[Unit]',
|
|
68
96
|
'Description=DevBridge Dev Runner',
|
|
@@ -206,6 +234,9 @@ export function unitIsBroken(readFile = (p) => fs.readFileSync(p, 'utf8')) {
|
|
|
206
234
|
const target = parts[0]?.endsWith('node') ? parts[1] : parts[0];
|
|
207
235
|
if (!target)
|
|
208
236
|
return false;
|
|
209
|
-
|
|
237
|
+
// Gone already, or living somewhere that will be gone after a reboot — the
|
|
238
|
+
// second one still runs today, which is exactly why it has to be repaired
|
|
239
|
+
// before the reboot rather than after it.
|
|
240
|
+
return !fs.existsSync(target) || isEphemeralPath(target) || isEphemeralPath(parts[0] ?? '');
|
|
210
241
|
}
|
|
211
242
|
//# sourceMappingURL=service-unit.js.map
|
package/dist/supervisor.js
CHANGED
|
@@ -12,8 +12,9 @@ import { VerifyRunner, runOneOffCommand, } from './verify.js';
|
|
|
12
12
|
import { VerifyReportQueue } from './verify-queue.js';
|
|
13
13
|
import { applySession, gitBranches, gitCommit, gitDiff, gitLog, gitPush, gitRefs, gitShow, gitStatus, revertApply, gitStage, gitUnstage, gitDiscard, gitPull, gitMergeAbort, updateFromBase, workspaceState, } from './gitops.js';
|
|
14
14
|
import { fsView } from './fsview.js';
|
|
15
|
-
import { agentAuthStatuses, AuthRelay } from './auth-relay.js';
|
|
15
|
+
import { agentAuthStatuses, AuthRelay, clearAgentAuthFailure, noteAgentAuthFailure, } from './auth-relay.js';
|
|
16
16
|
import { selfUpdate } from './self-update.js';
|
|
17
|
+
import { rememberWorkspacePath } from './environment.js';
|
|
17
18
|
import { composeMessageWithAttachments, saveAttachments, } from './attachments.js';
|
|
18
19
|
export class Supervisor {
|
|
19
20
|
ws;
|
|
@@ -297,6 +298,10 @@ export class Supervisor {
|
|
|
297
298
|
* would be a lock bought for nothing.
|
|
298
299
|
*/
|
|
299
300
|
async prepareWorkspace(descriptor) {
|
|
301
|
+
// Also here, not only at bind time: a server paired before this existed has
|
|
302
|
+
// never sent a `validate_path`, and its projects would be invisible to
|
|
303
|
+
// `doctor` until somebody re-bound them.
|
|
304
|
+
rememberWorkspacePath(descriptor.workspace.path);
|
|
300
305
|
if (descriptor.workMode === 'DIRECT') {
|
|
301
306
|
return prepareDirectWorkspace(descriptor.workspace.path);
|
|
302
307
|
}
|
|
@@ -794,7 +799,12 @@ export class Supervisor {
|
|
|
794
799
|
if (!running.session)
|
|
795
800
|
return;
|
|
796
801
|
running.parkRequested = true;
|
|
802
|
+
// `code` is what the dashboard reads to tell «parked» from «your turn»
|
|
803
|
+
// (#124). The prose stays for runners older than 0.23.0, which the
|
|
804
|
+
// dashboard still matches on; delete that fallback once the fleet has
|
|
805
|
+
// moved, not before.
|
|
797
806
|
this.sendEvent(running, 'system_note', {
|
|
807
|
+
code: 'session_parked',
|
|
798
808
|
text: 'Session parked — the runner switched to another session. Send a message to resume.',
|
|
799
809
|
});
|
|
800
810
|
running.session.stop('session_parked');
|
|
@@ -867,6 +877,15 @@ export class Supervisor {
|
|
|
867
877
|
});
|
|
868
878
|
return;
|
|
869
879
|
}
|
|
880
|
+
// The retry is spent and the agent is still refused — this is the only
|
|
881
|
+
// authority on a login the credentials file cannot see through (a
|
|
882
|
+
// provider-side revocation leaves the file looking perfectly healthy).
|
|
883
|
+
// The panel is told from here, not from a guess (#121).
|
|
884
|
+
if (isAuthCode(event.code)) {
|
|
885
|
+
const refused = relayAgent(String(descriptor.agent).toLowerCase());
|
|
886
|
+
if (refused)
|
|
887
|
+
noteAgentAuthFailure(refused);
|
|
888
|
+
}
|
|
870
889
|
// Forward the code: the API stores the payload as-is, so the dashboard
|
|
871
890
|
// can offer "Sign in" instead of a dead error card.
|
|
872
891
|
this.sendEvent(running, 'error', {
|
|
@@ -906,6 +925,13 @@ export class Supervisor {
|
|
|
906
925
|
}
|
|
907
926
|
return;
|
|
908
927
|
case 'message':
|
|
928
|
+
// The provider answered, so this sign-in works — drop any refusal we
|
|
929
|
+
// are still holding against it (#121).
|
|
930
|
+
if (event.role === 'assistant') {
|
|
931
|
+
const working = relayAgent(String(descriptor.agent).toLowerCase());
|
|
932
|
+
if (working)
|
|
933
|
+
clearAgentAuthFailure(working);
|
|
934
|
+
}
|
|
909
935
|
this.sendEvent(running, 'message', { role: event.role, text: event.text });
|
|
910
936
|
return;
|
|
911
937
|
case 'thinking':
|
|
@@ -1214,6 +1240,7 @@ export class Supervisor {
|
|
|
1214
1240
|
// Parked session: the follow-up message becomes the resume prompt.
|
|
1215
1241
|
if (!this.ensureCapacity(running.descriptor.id)) {
|
|
1216
1242
|
this.sendEvent(running, 'system_note', {
|
|
1243
|
+
code: 'runner_busy',
|
|
1217
1244
|
text: (this.maxSessions === 1
|
|
1218
1245
|
? 'The runner is busy with another session — this one continues as soon as it finishes its turn.'
|
|
1219
1246
|
: `The runner is busy with ${this.maxSessions} other sessions — this one continues as soon as one of them finishes its turn.`) +
|
|
@@ -1579,6 +1606,10 @@ export class Supervisor {
|
|
|
1579
1606
|
if (!path)
|
|
1580
1607
|
return void reply({ ok: false, error: 'path argument is required' });
|
|
1581
1608
|
const validation = await validateWorkspacePath(path);
|
|
1609
|
+
// Remembered so `devbridge-runner doctor` can check the permissions
|
|
1610
|
+
// of the real projects without being told which they are.
|
|
1611
|
+
if (validation.ok)
|
|
1612
|
+
rememberWorkspacePath(path);
|
|
1582
1613
|
return void reply({
|
|
1583
1614
|
ok: validation.ok,
|
|
1584
1615
|
result: validation,
|
|
@@ -2209,6 +2240,9 @@ export class Supervisor {
|
|
|
2209
2240
|
if (!agent || !code)
|
|
2210
2241
|
return void reply({ ok: false, error: 'agent and code are required' });
|
|
2211
2242
|
const result = await this.authRelay.submitCode(agent, code);
|
|
2243
|
+
// A fresh credential outranks anything we remember about the old one.
|
|
2244
|
+
if (result.ok)
|
|
2245
|
+
clearAgentAuthFailure(agent);
|
|
2212
2246
|
return void reply({
|
|
2213
2247
|
ok: result.ok,
|
|
2214
2248
|
result,
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const RUNNER_VERSION = "0.
|
|
1
|
+
export declare const RUNNER_VERSION = "0.27.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
package/package.json
CHANGED